Performance in React Native
Introduction
In this article I will focus on a topic that is quite often overlooked by React Native developers, namely – performance.
React Native is quite efficient and its default capabilities are sufficient in most scenarios, but if, for example, you are going to create a game in it, you probably should get deeper and do your best in order to make your product run smoothly.
React Native Architecture
First of all, you should know the basics of RN. It has three basic threads:
- UI – the main thread of RN application. It is responsible for rendering native views that are specific for the platform you build for.
- JS – your whole business logic works here
- Shadow – thread responsible for calculating the layout based on JSON data given by JS thread. Then processed data is sent to the UI thread that uses it to render native components to the screen. It is crucial to mention that such components do not use flex-based styling, so they also need to be converted here. It is done by Facebook’s Yoga Engine.
There are also Native Modules which on iOS have one thread assigned to each module and on Android they share one system thread.
The communication between JS side and Native side takes place over the asynchronous bridge. Consider an example – a developer wants a button to toggle the background color from red to blue each time it is pressed. The communication scheme would look as follows:
- JS wants to render red button with a handlePress function assigned to the onPress parameter
- JS serializes the data and sends it over the Bridge that sends it asynchronously to the Shadow thread
- Shadow thread calculates the layout and sends this calculated data to the UI that renders the button to the screen
- UI thread transmits serialized notification of successful rendering process to JS (of course over the Native Bridge!)
- User presses the button
- UI thread triggers handlePress callback over the Bridge
- handlePress change button color to blue on JS side and triggers rendering process as it was mentioned in points 1, 2, 3 and 4
So now you know how RN works under the hood. Let’s move on with the topic and discuss some practical performance gaps.
Animations
Consider an example – you want to create some layout animations in your app to improve the user experience but after implementation you noticed that instead of working flawlessly it turns out to be sluggish in some parts of the application. That is probably caused by you forgetting to set useNativeDriver to true!
You are now likely to ask “why?”. So, let’s explain first how it works when the useNativeDriver is set to false. In this case all required calculations are done on JS side, then their results are sent to the UI thread over the Native Bridge and so in every frame. Thus, when your app needs to perform some expensive logic parallelly with animations it may result in animations framing due to JS thread being blocked.
What native driver does is move all calculations to the UI thread. JS is only responsible for sending all required data before an animation starts and the rest of work is done by native side.
Useless re-renders
Imagine a situation where a developer, for the purpose of this example named Jon, was requested to participate in a project aimed at creating an economic game similar to SimCity. After some planning, when it came to task distribution, Jon was given a crucial task to implement the user map rendering feature. He started with creating a Level component, then he added some position-calculating methods and at the end, in the return statement, he mapped an array with building’s data to images visible on the screen. Some time after that, he was moved to another project. After a while another dev – Bob, was asked to add a functionality of placing new buildings on the map. He decided to make a new array with construction sites’ data and then map that in Level just a line below the place where regular buildings were mapped.(At this point you probably already know what Bob did wrong) During tests it turned out that after a user placed a new construction site, the whole application got stuck for at least one second (time depended on the map size).It was caused by the fact that the whole Level component needed to re-render after each change in the construction sites’ array. So Bob had to look at it again. Finally, he moved each mapping to a separate component with its own connection to the store and that solved the problem. There was no need for Level to reload since the props related to buildings or construction sites were no longer there. Level component knew only that it renders two other components but it was not aware of the things they do or why.
A lesson you can learn from this is that you should always try to atomize your project structure to the smallest logically linked parts. Try to make your code as orthogonal as possible. And of course test your code deeply before you release it :P.
If you do not know what orthogonal code means read a bit about aspect programming paradigm.
It is React’s nature that when you update parent component the child ones are also re-rendered, but you can change that behavior. Use React.PureComponent for class-based components or React.memo for functional ones. PureComponent has its own implementation of shouldComponentUpdate which after shallow comparison of previous and future props decides whether to update the component or not. In case of memo it is not that simple, there is no built-in comparison method you can use, instead you ought to write it yourself. But there is a hacky solution for that. Redux connect function has its own implementation of shouldComponentUpdate enabled by default. So if you are using a store connection in your component you do not have to be wary about that anyway.
Even though you used any implementation of shouldComponentUpdate, your code may be exposed to unnecessary re-renders. If you pass inline function as a prop to component with shouldComponentUpdate implemented, each time the parent updates, the child does it too. It happens so because each render creates a new function. Shallow comparison in the non-primitive type is done by reference, so (() => {}) === (() => {})
will always return false and trigger re-render.
In class-based components, the solution for that would look as follows:
In functional components it is more tricky since each nested function is recreated after each parent’s re-render. There are two solutions to that:
- simply move the function outside the component
- pass your function as a parameter to the useCallback hook
Here is a real life example of RN performance optimization capabilities from our recent project – Enter the game. Such a difference is made by changing the project structure to more orthogonal and not using inline functions as props.
You can read more about the game.
Redux repeatedly reevaluating the selectors
Imagine a screen like this:
At this point I assume you know the basics of data flow and setup in redux.
So what we have here is a simple screen, connected to the redux store. After pressing any of the buttons, the counter’s value will change accordingly. Note that increment function contains a call to the dispatch function twice.
When you inspect this code for example with RN debugger from Webstorm and add breakpoints in return lines of both CounterScreen and mapStateToProps you will find out that after pressing the increment button, mapStateToProps reevaluates twice (once for each dispatched action) but CounterScreen re-renders only once. Starting from redux@7.0.1 dispatches are batching together to prevent additional re-renders, but unfortunately after performing a single action, each rendered selector in the app is still notified ( the connect function).
To prevent this, you can use an external store enhancer such as redux-batched-subscribe
Remember to add batchedSubscribe as the last argument of the compose function since it is applying the functions passed on to it from right to left.
When you debug the app again, you will see that after pressing increment button, mapStateToProps will reevaluate only once.