React Native Performance Optimization and Profiling

Marcus Osterberg
ITNEXT
Published in
7 min readMar 24, 2018

--

React Native is a popular technology for engineers who are looking for building mobile applications in a productive way without compromising on native performance.

Devices today comes with multi core CPU’s and large amount of RAM. This is great for processing large data sets, computation intensive tasks, rich interactive UI’s, AND our apps will forgive us for not even consider performance optimizations. This does not mean that you should not keep performance in mind when developing a mobile application.

I will share my experience on performance issues in React Native that one should be aware about. This is all based on experience from building large performing mobile applications at InterNations using React Native. I will highlight common pitfalls and give a general understanding of what can go wrong and how to solve issues. 🖥 +🕵️‍♀️ +🔧 = 🚀.

In case of building a small React Native app the likelihood for running into performance issues is of course rather low and I would not recommend prematurely optimizations.

State of React Native performance

React per default re-renders a component every time its parent receives new props or updates its state, that’s one of many reasons why the React paradigm for building UI’s is superior. This is also the cause for wasteful re-renders.

In React Native we can update the frames 60 times per second. This provides the user with a smooth native experience. As the complexity of an app grows so does computation. To achieve a constant 60 FPS we would need to make sure that our UI is never blocked. Unfortunately JavaScript runs in a single-threaded enviroment in comparision to Swift/Objective-C for iOS or Java/Kotlin for Android which all supports multiple threads. Luckily many of the standard React Native components executes on a separate thread known as the main thread. This is really good because when the JavaScript thread is busy, the UI will still feel responsive.

One expensive user interaction can be navigation. Imagine a user is switching between multiple screens. Every screen is preforming network requests, computing data, screen transitions and navigation animations. All of it taking place on the JavaScript thread, then the apps frame rate would drop significantly (depending on complexity). I highly recommend to fully outsource navigation to the main thread, it will dramatically improved the overall performance. Look for a native navigation library solution.

In React Native there is the concept of a bridge, it’s what connects the native world and the JavaScript side. The two of them communicates by sending and receiving data in a JSON format. Sending data back and fourth over the bridge can be very expensive because the data needs to be serialized and deserialized every single time.

Testing performance issues

Current frame rates for both threads can be monitored using the perf monitor which can be located under the developer menu.

Measuring how many times your components are rendering is absolutely key. An easy way is to simply add a console.count call in your render method.

render() {
console.count('component')
return <Component />
}

If you are running RN version 0.57 or later you can make use of React Profiler.
See docs for how to setup react-devtools and read this post on how to profile rendering of components.

Both latest Android Studio and Xcode versions offers profiling tooling. Try to diagnose your memory consumption and CPU load during runtime. Navigate or run certain activities in the app to verify that memory is being allocated to the heap and removed again (hopefully) once no longer in use. Also keep an eye on CPU spikes to identify computation heavy tasks. See official documentation for more in depth explanations about the platform specific profiling tools Android or iOS.

Building software without looking out for memory leaks will result in software with memory leaks

For Android apps running low on memory you might want to request a larger heap size by adding the largeHeap attribute in the android manifest file. See more details here.

If you are offloading work from JavaScript to the native side prevent the bridge from becoming overloaded by monitoring how many times messages are sent and received over the wire or how large the payloads are. The MessageQueue lets you invoke a spy method to observe the traffic on the bridge. A good starting point is to identify messages sent over the bridge containing little to zero sized payloads and see if you can remove or replace the source of the message. E.g. empty views used to wrap multiple elements could be replaced with React Fragments.

import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'MessageQueue.spy(message => console.log('Message', message))

Solving performance issues

Keep the unnecessary re-renders down with React.PureComponent or memo. A pure component (not to be confused with stateless function components) will shallow compare its props to determine if a re-render is necessary. For more fine-grained control you should use the shouldComponentUpdate method.

Sometimes we want to re-render more frequently e.g. imagine you have a component that renders a list of items and the list receives new props from different user inputs like pagination or pulling down the list to refresh its data. Avoid inline functions in the render method, they will be re-created on every render and leads to components re-rendering because of prop changes, to reduce this cost name the function and store it outside of the render.

class Component extends React.Component {
renderSectionHeader = () => <View style={...} />
render() {
return (
<FlatList
renderSectionHeader={this.renderSectionHeader}
{...this.props}
/>
)
}
}

Learn more about different techniques and concepts on how to avoid pointless re-renders in this article.

If you are lifting up your state to a global level, then the side effects for a state change is potentially larger. For us any many others that are using Redux and React Connect as a state management tool, should watch out for selectors re-running on every time a state change occur. A great way to protect selectors against expensive computations is by caching them so that they return the previous result of a state change if its references to state has not changed. This can be easily achieved with a handy library called Reselect.

If you still consider to handle navigation on the JavaScript side or you are struggling with frame drops during animations. Consider to give InteractionManager API a chance e.g. defer the render of a component until all user interactions or animations has ended.

class AfterInteractions extends React.Component {state = { interactions: true }

componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this.setState({ interactions: false })
}
render() {
if (interactions) {
return null
}
return this.props.children
}
}

Animations in React Native looks good and are easy to create. Using the Animated library lets you enable native driver, this will send the animations over the bridge to the native side before the animation starts. Your animations will now execute on the main thread independently of a blocked JavaScript thread, this will result in less frame drops and a smoother experience. Set useNativeDriver to the animation configuration.

Animated.timing(
this.state.fadeAnim, {
toValue: 1,
useNativeDriver: true,
}
).start()

Last but not least, images. Reduce file size of images makes sense to save users data plan, speed up network requests and reduce memory footprint. Webp is a great compression format to consider using. Moving static images to the native side will reduce the JavaScript bundle size which is a good thing 👍 (however you won’t be able to serve clients your images on the fly any longer). More details on this topic can be found here.

Developing in React Native gives you the benefit of cross targeting platform developing. Building for Android and iOS simultaneously. In our experience the re-usage of code between both platforms has been astonishing. The memory layout on iOS is different compare to Android (automatic reference counting instead of garbage collection) this helps iOS apps to have a smaller memory footprint. We have noticed that iOS runs majority of the times faster then Android and consumes less memory. I recommend that if your goal is to release on both platforms then prioritize Android. I wish this was not a thing but based on our experience if Android runs smooth so will iOS.

Regardless of platform one should always be testing on a real device constantly during development. When using a simulator, make sure that on Android to activate hardware accelarator it will drastically improve the performance of your simulator.

Overall experience so far with React Native regards to performance and optimization tooling has been positive. You learn as you go and there will always be areas of your app to improve upon. I’m looking forward to continue developing in React Native and to see how the community continues to improve the this great piece of technology. One improvement I would like to see in React Native soon would be support for multi threading without the need of bridging data. I could imagine something like workers for JavaScript that we have on the web.

I hope you’ve now learned something new that will help speeding up your native app!

I would be happy to answer questions or to discuss any of the topics mentioned here. Drop a comment below or find me on twitter.

--

--

Swedish. I like to code, learn new things and travel new places.