Dealing with performance issues in React Native

Martynas Bardauskas
Wix Engineering
Published in
7 min readJan 17, 2019

React Native has a bad reputation when it comes to performance, which isn’t always necessarily React Native’s fault. I’d like to share some thoughts I have on this.

Why Should Developers Care About Performance?

First of all — user expectations. Here are some interesting statistics. Half of your users expect the app to load in 2 seconds or less. More than half of your users blame the app for being slow, not their device. Four out of five users will only attempt to use a problematic app 3 times or less before removing it.

Furthermore, mobile devices are used more than desktop machines. The difference is slight and it depends on the time of the day, although the tendency clearly is for people to use mobiles more. In future, they’ll probably be used even more than desktop.

What’s React Native to a Web Developer?

I think the best way to illustrate how React Native is different from React for Web Developer, is to actually show two pieces of code:

React Native is the same React, though there are some slight differences mainly that different components are being used. Instead of the div you have a View, instead of a button you have a TouchableOpacity. And all texts have to be wrapped to Text component. Most of the other things, including store (meaning redux, mobx) are the same. Sending network requests is the same. Even LocalStorage is very similar, it’s called AsyncStorage. You’re just in a Mobile App instead of browser environment.

What’s “Native” about it?

These are things that are usually not taken into consideration when people from the Web make the switch to Mobile. Code is more or less the same and people tend to stick to what they know best. Furthermore, on average, even the Web is struggling operate smoothly with 60 frames per second while scrolling. Meaning, they experience performance problems as well, though they aren’t that noticeable.

The issue with Native apps is that even though users have higher expectations for their apps than the Web, they have weak CPUs, low memory and low disk space. You may argue that your phone is very powerful, but I believe if we compare any of your devices they’d still be far worse than your MacBook Pro or whatever you have as a laptop.

Furthermore, users care about their battery life. They don’t want you to drain their battery by running useless features.

Mobile data is costly, people pay for their data plans and they don’t want you to drag insanely large data sets, especially if you don’t need them. Furthermore, their latency is a lot higher. For example, average 3G network has a latency of up to half a second. That’s a lot — try playing an online shooter with this kind of latency :-).

What’s more is that even if they have a bad network connection, they still don’t expect loaders. Or in other words, they don’t expect Web experience. They also expect the app to work offline. How many Single Page Apps or Progressive Web Apps have you’ve built that support offline mode? Probably not many. I know I haven’t.

Users expect real-time updates. Not a very common pattern in the Web. App has to be smooth, intuitive and contain animations.

With Web, you can just ship your changes any time you want. In Mobile, the process is a lot different, and it takes at least few days to push out a fix. Furthermore, users won’t necessarily update. Some can’t, because of hardware, disk space or whatever. Some won’t, for various reasons. Some don’t want to because they believe each update is making their phone slower.

What About Native Mobile Developers?

It’s different for Native Developers. Most of them are inherently aware of all the things I’ve just mentioned, but their environment changes drastically. First of all, they’re moving from imperative to declarative programming. In case you don’t know, in short, imperative is “how” things are done, while declarative is writing “what” should be done. Also, they’re moving from multi-threaded environment to single thread. Furthermore, everything is running in UI Thread. Which means that they’re no longer able to defer heavy operations. Or that is hard to achieve. They used to be able to get native SDKs directly, now it’s via other node_modules. Not to mention the additional layer of dependencies they have to manager — both Native and npm.

I believe the one of the biggest changes is that React components don’t directly represent View structure in Native, and thus they’re left with less wiggle room in React Native. Don’t take my word for it though, I come from a Web background :-).

What Can Be Done?

Lazy Require

A lot actually. Lazy require should help you with App start time. Instead of importing dependencies at the beginning of the file, do the require only once you need that dependency. Or enable inline requires which will do the work for you during bundle time. Either way — be idle until urgent. Note that this is getting more important as the code base gets bigger. Each megabyte in your bundle size (this includes your dependencies) will slow down the initial start of your app.

Avoiding wasteful renders

There are usual suspects for wasteful renders. For example, you should implement a shouldComponentUpdate method for your React components, or at the very least, use PureComponent instead of Component where appropriate, which does a shallow comparison for props.

Use memoization. It’s a caching mechanism which caches outputs for different sets of inputs. Meaning, if you give the same arguments for your memoized functions, it will return the same values. It’s very useful for redux-connect functions, because whenever you take something from the store and run filter on it, it will create a new instance every time and will cause a re-render with each store change. You can use reselect for redux stores. It should be a store-agnostic mechanism, and should work with others, though it might require some tweaking.

Arrow functions is another usual suspect for wasteful re-renders. Don’t use arrow functions as callbacks (such as click/tap) in your render functions. With arrow function, each render creates a new instance of that function, so when reconciliation happens, React does a diff and says ok, this is new, because the function reference doesn’t match.

The same goes for Style References. If you use objects/arrays for styling, they will create new instances with each render. You should use StyleSheet from React Native which always passes a reference. You might ask what you should do with dynamic styles? Well, you can use it for now, because the main issue is that we’re using the object/array constructs even though we don’t need to.

It’s important to note that following the best practices I’ve mentioned above will probably have a minor impact on overall performance of your app. That, of course, depends on your current performance and coding practices in general. I do believe we should still follow them (even in the Web) because it will reduce the risk of slowing your app and the code itself may be cleaner in the end.

Network related improvements

Batch network requests. In Wix we have node platform where we, as client developers, can create a middleman between our Scala services, thus we are able to batch our network requests. Meaning, do only one request from the app and combine the data in the middleware server. That usually works quite well with GraphQL. Keep in mind though, I would think twice before adding GraphQL to your stack when building a new product since you’d be facing issues of adopting a new technology along with other project start-up headaches. REST should work perfectly fine as well.

Instead of polling, use WebSockets. This is a usual pattern for chats, but there are more use cases. In short, WebSockets will work whenever you have to send requests to your servers every few seconds or minutes to get updates.

Use small resources. I mean mainly images here, though if your responses are big and you’re not using the data you download, you should optimize those resources as well.

Move heavy computations to back-end. Map and reduce functions in your code are fine in most cases, unless you’re doing that for thousands of items. If you’re experiencing extreme situations and extreme amounts of data, avoid operating with them on the device and move it to the back-end.

Use device storage & cache data. Use it to cache the data so once people turn on the app, they’ll first get it from local storage and update only when needed. This will also help to support offline mode.

Use pagination for lists. Get the small amount of initial data, make sure that the items are rendered via FlatList, otherwise you’ll get performance issues.

Measure Performance

I should have written this chapter first, since you have to measure performance before you try to improve it. However, I haven’t, because I believe measuring techniques and tooling deserve a separate blog post altogether. Therefore I’ll just say that you can start by reading Profiling chapter in React Native docs. You can also try out React Profiler in Chrome DevTools (works with React v16.5+). Just beware that it won’t give you accurate results as the code is running in Chrome, but will give you a general idea of where bottlenecks are. Furthermore, I encourage you to try DetoxInstruments to measure your React Native app’s performance, it’s one of the tools we use at Wix.

Conclusion

While React Native has it’s impact on overall App performance, it is still quite a fresh technology and its creators are working hard to improve it. Thus, we should also do our part and improve our Apps where we can. Both these efforts will produce React Native Apps that our users are delighted to use.

--

--