React Native Measuring and Improvising App Performance

Rahul Mahato
Engineering @ Housing/Proptiger/Makaan
9 min readApr 27, 2023

TL;DR — Measuring React Native performance can be done by using tools such as the Flipper and the React Native Debugger. To improve performance, optimize images, use Virtualised Lists instead of ListView, reduce the number of re-renders. Additionally, using off-main-thread animations. Also, Keep in mind that test on actual device, not just on emulator as it can give more accurate results.

Image by svstudioart on Freepik

React Native is a well-liked framework for building mobile apps as it allows for efficient development without sacrificing native performance.

It’s important to keep in mind that despite the capabilities of modern devices with multi-core CPUs and large amounts of RAM, performance optimization should still be considered during the development process of a mobile application in order to provide a smooth and efficient experience for end-users. Even a small app can have performance issues if we do not consider the best practices, and it’s better to address them early on.

First let us look into how react native works under the hood

React Native Bridge

A React Native App Comprises of two main sides

  1. The Native/UI Side [ Java/Kotlin for Android & Swift/Objective-C for iOS ]
  2. The JavaScript Side

React Native allows us to create a bridge between the JavaScript code and Native Side. This bridge allows the JavaScript code and Native Code to interact with each other.

Working of the bridge :

When we hit any button or interactive element on the App the interactions are accepted by the UI Thread and sent to the Javascript thread. The Javascript thread computes the Layout changes and redirects them back to the UI Thread via this Bridge.

Re-renders

React tries to re-renders a component and its children every time its receives a new prop or update its state. This is one of the major paradigm for which a lot of engineers prefer React.

React has to “re-render” the entire component tree if the state changes. If you have a lot of components on the screen, or if your components are particularly complex, this can lead to some serious performance problems.

Fortunately, we can help reduce the number of re-renders that React has to do, thus improving the performance of our app. To do this, we can use a technique called shouldComponentUpdate() for class based components and wrap our functional components with React.memo(Component) and pass arePropsEquall function as its second parameter . This allows us to provide a condition that React must meet before re-rendering a component or its children. This can help to avoid unnecessary re-renders and make our app more efficient.

We can also use two other hooks to avoid unnecessary re-renders — useMemo() and useCallback(). This helps to minimize the number of times a component is re-rendered.

In summary, reducing re-renders is key to keeping our React app running smoothly. With the use of shouldComponentUpdate(), useMemo(), useCallback() and memoization, we can help ensure that our React app is running as efficiently as possible.

Pitfalls

When using shallow comparison in React, there are a few potential pitfalls that you should be aware of. Here are few of the important ones :

// Boolean Comparisions
true === true // Output: true
false === true // Output: false

// String Comparision
"" === "" // Output: true
"abc" === "def" // Output false

// Number Comparision
1 === 1 // Output: true
1 === 2 // Output: false

// Object Comparision
let user = { name: 'Rahul' };
let userCopy = user;
user === userCopy // Output: true
user === { name : 'Rahul' } // Output: false [ as both the objects have a different references]

/* x
* so, <Component user={{ name : 'Rahul' }} />
* will always send a new reference of the prop user
* which would lead to rerendering the Component again and again
* even if the name doesn't changes
*
*/

// Function returning a function
const onClickHandler = (item) => () => {}
onClickHandler(1) === onClickHandler(1) // Output: false
/*
* onClickHanlder would return a new function reference on every call,
* so, <ListItem onPress={onClickHanlder(item)} item={item} />
* will always send a new reference of the handler function to the ListItem component
* this is similar to writing inline functions
*
*/

useMemo & useCallback hooks

So, now that you know why it’s important to prevent unnecessary re-renders in React Native, let’s talk about how you can do that. There are two built-in hooks in React, useMemo and useCallback, that can help us with this.

  1. useMemo is a hook that lets you memoize a value. That means it will store the value for future use and only recalculate if one of the parameters given to useMemo has changed since the last render. This means that your component won’t have to re-render unless the value has actually changed.

1.1 Memoizing a value :

1.2 Memoizing a React Node :

2. On the other hand, useCallback is a hook that allows you to memoize a function in order to increase performance. It’s like useMemo except for functions instead of values, which can be helpful if you want to pass a function down as props — like an event handler — and don’t want it to trigger unnecessary re-renders in the child component.

By using these two hooks together with memoisation techniques, your React Native apps should be able to run at top speed without triggering unnecessary re-renders!

How do we know what to optimize?

When it comes to performance optimization, we want to make decisions based on data. The data comes from measuring performance using specialised tools. The process is often referred to as profiling.

There are many tools available that can help us with profiling our React Native apps: react-devtools, why-did-you-render, Profiler, and others.

Flipper

Flipper is a good platform for debugging Android, iOS and ReactNative applications. Moreover it has React Devtools Profiler integrated as a plugin that can produce a flame graph of the React rendering pipeline as a result of profiling. We can use the data to measure the re-rendering issues of our application.

Once you have downloaded Flipper select React DevTools plugin ( this is also present in Redux Debugger but that tool becomes quite slow when your state is huge and have a lot of updates ).

Flipper UI — React DevTools

Make Sure you have turned on “Record why each component rendered while profiling” option is enabled in the settings icon.

Record why each component rendered while profiling Option screenshot
React DevTools Setting

Hit the Round Blue icon to start the profiler then run the flow which you would like to profile and once done you may hit the Round Red Icon to stop the profiler.

A flamegraph like the one below would be generated by the Profiler which you could use to understand the bottlenecks of re-renders.

Here we get to see the Components which have rendered multiple times along with the time they took and the reason for each render.

To start with you can choose the component which are render heavy and rendering multiple times.

Best Practices for Boosting React Native Performance

High performance is essential for creating a smooth user experience, and there are a few things you can do to make sure your app is running as optimally as possible.

  • First, you could memoize your selectors when dealing with Redux states, Reselect is a library that might come handy. This helps prevent unnecessary re-renders by caching the results of an expensive computation and returns it quickly.
// ❌ For base selectors, make sure that we dont send new array/objects references from selectors
const getItemList = state => state.items.list || []

// ✅ instead create a array/object reference in the global scope and use the reference
const EMPTY_ARRAY = []
const getItemList = state => state.items.list || EMPTY_ARRAY

/*
* This is important as when we dont have lists the selector would
* be returning a new reference on every call to the component
*/
  • You can also use Reanimated for declarative UI animations instead of React Native’s built-in Animated API, which will help improve the responsiveness of animations in your app. Furthermore, it’s important to avoid inline styles, functions, objects or array props in components since they will cause React Native to create a new version every time they change, triggering unnecessary rerenders.
  • Inline styles can cause a new object reference on each render, which can lead to re-rendering the whole component tree unnecessarily. This can have a negative impact on performance and should be avoided when possible. To ensure optimal performance, developers should use inline styles sparingly and only when necessary.
    A few bad style practices
const styles = StyleSheet.create({
container: {
flex: 1
},
backgroundRed: {
backgroundColor: 'red'
}
});

// inside the component
return (
<GenericView styles={[styles.container, styles.backgroundRed ]}>
...
children here
...
</GenericView>
)

// ❌ This would generate a new Array reference on every render the component


{/*
* ✅ You could solve this by creating a separate style
* this one outside the component lifecycle
*/}

const genericContainerStyle = StyleSheet.compose(
styles.container,
styles.backgroundRed
)

// inside the component
return (
<GenericView styles={containerWithBackgroundRed}>
...
children here
...
</GenericView>
)

Sometimes you need to have conditional styles based on some props, the above method wont work in that case, so you need to memoize the calculated style based on the props.

const styles = StyleSheet.create({
container: {
flex: 1
},
backgroundRed: {
backgroundColor: 'red'
}
});

// inside the component
return (
<GenericView styles={[styles.container, isExperimentRedBG && styles.backgroundRed ]}>
...
children here
...
</GenericView>
)

// 👆 ❌ This would generate a new Array reference on every render the component


{/*
* 👇 ✅ You could solve this by memoizing the style using useMemo
*/}

const createStyles = ({ isExperimentRedBG }) => StyleSheet.compose(
styles.container,
isExperimentRedBG && styles.backgroundRed
)

// inside the component
const componentStyles = useMemo(() => createStyles({
isExperimentRedBG: props.isExperimentRedBG
}), [ props.isExperimentRedBG ])

return (
<GenericView styles={componentStyles}>
...
children here
...
</GenericView>
)
  • Finally, breaking down larger components into smaller ones will also help boost performance because React Native only needs to rerender only the parts that have changed rather than the entire component. All in all, taking these steps are key if you want to maximize your app’s performance and create a better user experience.
  • In addition to this, you can also use the React Native profiler to measure the performance of your app. This tool allows you to inspect the performance of memory, components, and the number of frames per second (FPS). With the profiler, you can identify parts of the code that need optimization and make the changes accordingly.
  • You can also use the Chrome developer tools to inspect memory usage in React Native. This will help you identify memory leaks and determine how much memory your app is consuming. Once you’ve identified issues, you can use React Native’s tracing API to optimize the code for better performance.

We also migrated from react native’s FlatLists to FlashList by Shopify and found that it improved list-rendering performance significantly. FlashList is based on RecyclerListView, which provides excellent scrolling performance without the excessive re-renders of Virtualised List, and can be more efficient for large datasets.

Ultimately, these best practices and tools are helpful for improving the performance of your React Native app. If you’re serious about creating a great user experience, taking the time to optimize your app will be worth it in the end.

Conclusion

All in all, by following the tips mentioned in this article, you can go a long way towards improving the performance of your React Native app. And with a bit of extra effort, you can even prevent costly re-renders from occurring.

--

--

Rahul Mahato
Engineering @ Housing/Proptiger/Makaan

React Native developer @Housing.com, passionate about creating efficient, high-performing web and mobile apps that make a difference.