Editor’s note: This React Native article was last updated on 25 October 2022 to include information on the react-native-fast-image library and code examples of the react-navigation and react-native-navigation libraries.
A great user experience should be the core objective of any app development. Although React Native tries to provide everything you need to develop a performant application, there are occasions where you have to manually optimize your app. To do this, developers need to have a performance optimization mindset from the start of their projects.
Most modern mobile devices display 60 frames in one second — meaning you have 16.67 milliseconds to display a frame for the app to stay performant. Anything short of this will result in poor performance, and the UI may even appear to be unresponsive.
In this article, we will review how to optimize the performance of your React Native app using the following steps:
FlatList
or SectionList
to render large lists in React NativeInteractionManager
and LayoutAnimation
Reselect
with ReduxA React Native application is made of two parts: the JavaScript codes and the Native codes. These two are completely different and cannot ordinarily communicate with each other on their own.
They can only do so through the React Native Bridge (sometimes known as the Shadow Tree). The Bridge sits between the JavaScript codes and the Native codes and sends serialized JSON objects from the JavaScript thread to the Native thread and vice versa.
Although Facebook is working on an improved React Native architecture (and a reason why you shouldn’t give up on React Native) that would enable the JavaScript codes to talk directly to the Native codes through the JavaScript Interface (JSI). Performance issues can easily occur in the current architecture when dealing with complex processes. This is why it is important to optimize your React Native application before shipping it to production.
Let’s look at some of the best practices for improving the performance of a React Native application with several code examples.
FlatList
or SectionList
to render large lists in React NativeIf you have a large list, rendering all the items at once can cause a performance issue, but lazy loading with FlatList
can improve performance.
The FlatList
component renders only the items that will be displayed on the screen and removes them when they are no longer displayed. This saves a lot of memory, making the app much faster:
import React from 'react' import {FlatList} from 'react-native' const data = [ { id: 1, text: 'First' }, { id: 2, text: 'Second' }, ... ] const App = () =>{ const renderItem = ({item}) =>( <View> <Text>{item.text}</Text> </View> ) return ( <FlatList data={data} renderItem={renderItem} keyExtractor={item => item.id} /> ) }
FlatList
and SectionList
serve similar purposes. Both can improve the performance of your app. However, SectionList
is more suitable when rendering sections. VirtualizedList
may also be used if you need more flexibility.
It is also possible to render lists with ListView
and, while this can be used for small lists, it is not recommended for large lists. Although it is possible to render lists with map
, it is not advisable to do so in React Native.
Console statements are necessary for debugging JavaScript codes, but they are only meant for development purposes. These statements could cause serious performance issues in your React Native application if they are not removed before bundling.
While you could install plugins such as babel-plugin-transform-remove-console
to remove these statements from production, it is better to manually remove them if you don’t want to add additional dependencies to your app.
React introduced the memo
HOC (Higher Order Component) in React v16.6 for preventing unnecessary re-rendering and the useMemo
hook in React v16.8 for optimizing expensive computations.
However, it is also possible to use the useCallback
hook to do that. The major difference between useMemo
and useCallback
is that useMemo
returns a memoized value, but useCallback
returns a memoized callback.
Let’s take a look at each of them.
React.memo
Higher Order Component (HOC)React.memo
was introduced to functional components to serve the same purpose that React PureComponents
serve in class components. memo
prevents unnecessary re-rendering of a component and can help to optimize an application.
However, like other optimization technique, memo
should only be used when it is necessary. In some cases, unnecessary re-rendering will not impact performance much.
Here is an example to illustrate memo
:
import React from 'react' import {View, Text, TouchableOpacity} from 'react-native' const Element = ({children, value, setValue}) =>{ const handleOperation = () => setValue(value * 2) return ( <View> <Text>{value}</Text> <TouchableOpacity onPress={handleOperation}> {children} </TouchableOpacity> </View> ) } export default Element import React, {useState} from 'react' import {View} from 'react-native' import Element from './Element' const App = () =>{ const [firstNum, setFirstNum] = useState(5) const [secondNum, setSecondNum] = useState(5) return( <View> <Element setValue={setFirstNum} value={firstNum} > Add First </Element> <Element setValue={setSecondNum} value={secondNum} > Add Second </Element> </View> ) }
The problem with the above code is that when any of the buttons are pressed, both buttons will re-render even though only the states for the pressed button will be changed.
This can be fixed by wrapping the Element
component with the React.memo
HOC. Here’s how to do that:
import React, {memo} from 'react' import {View, Text, TouchableOpacity} from 'react-native' const Element = ({children, value, setValue}) =>{ const handleOperation = () => setValue(value * 2) return ( <View> <Text>{value}</Text> <TouchableOpacity onPress={handleOperation}> {children} </TouchableOpacity> </View> ) } export default memo(Element)
This would fix the re-rendering issue. However, it should be used only when the re-rendering is causing performance issues.
useMemo
hookuseMemo
returns a memoized value of a function. However, it should only be used when performing expensive computations.
For instance, suppose we want to filter some data coming from our API by their rating. We could memoize the computation to recalculate the results only when the values change:
const data = [ {id: 1, state: 'Texas', rating: 4.5}, {id: 2, state: 'Hawaii', rating: 3}, {id: 3, state: 'Illinois', rating: 4}, {id: 4, state: 'Texas', rating: 5}, {id: 5, state: 'Ohio', rating: 4.5}, {id: 6, state: 'Louisiana', rating: 3}, {id: 7, state: 'Texas', rating: 2}, ... {id: 1000, state: 'Illinois', rating: 4.5}, ]
If we wish to filter the data based on the rating (without memoization), we may use up a lot of memory.
For such, we don’t want to unnecessarily recalculate the values when other components re-render. We want to re-render or re-calculate only when the dependent rating changes.
Let’s see how we can achieve this with useMemo
:
import React, {useMemo} from 'react' import {FlatList} from 'react-native' import data from './data' const App = () =>{ const rateCompare = 3; const computedValue = useMemo(() => { //supposed computationally intensive calculation const result = data.filter((d) => d.rating > rateCompare); return result; }, [rateCompare]); const renderItem = ({ item }) => ( <View> <Text>{item.state}</Text> </View> ); return ( <FlatList data={computedValue} renderItem={renderItem} keyExtractor={item => item.id} /> ) }
We assumed that we have huge data coming from our API and needed to perform a computationally intensive calculation. Although we’ve used a simple filter operation, we could be doing a series of calculations here. This is a good use case for the useMemo
hook.
By using useMemo
, we can cache (memoize) the results for the value specified in the dependency array. For instance, if the rateCompare
constant was 3
the first time it is run, the function will not recalculate anytime the value of rateCompare
is 3
, even if the entire components re-render. It will only recalculate when the value changes.
useCallback
hookThe useCallback
hook is similar to useMemo
, but it returns a memoized callback:
import React, {useState, useEffect, useCallback} from 'react' import {FlatList} from 'react-native' import data from './data' const App = () =>{ const [values, setValues] = useState([]); const rateCompare = 3; const valuesCallback = useCallback(() => { //supposed computationally intensive calculation const result = data.filter((d) => d.rating > rateCompare); setValues(result); }, [rateCompare, setValues]); useEffect(() => { valuesCallback(); }, [valuesCallback]); const renderItem = ({ item }) => ( <View> <Text>{item.state}</Text> </View> ); return ( <FlatList data={values} renderItem={renderItem} keyExtractor={item => item.id} /> ) }
This does the same thing as the useMemo
example. However, because useCallback
returns a function, we need to call that function to get the value. Here, we have called the function in a useEffect
hook and then rendered the values in a FlatList
component.
We could equally have called the function in a Button
or TouchableOpacity
component. That way, the computation will run whenever the button is pressed:
Button onPress={valuesCallback} title="Example button" />
While React.memo
can optimize an entire component, useMemo
and useCallback
can optimize a calculation or process. However, each of these should be used only when they are necessary, otherwise, they could even compound the performance issue.
It is recommended to first write the calculation or component without memoization and only optimize (memoize) it if it is causing performance issues.
Images can contribute significantly to performance issues in React Native applications. They can also pose issues in web apps, but the browser has the capability of downloading and even scaling the images. In some cases, it may even cache them.
But this is different with mobile apps. React Native ships with an Image
component that can handle a single image very well, but it performs poorly with many large-sized images.
The best way to solve this problem is by loading the exact size of the image you need. In other words, you should resize and scale down the image size before loading them to your app.
Caching is another solution to image problems in a React Native app. It saves the images locally the first time they are loaded and uses the local cache in the subsequent requests. This could improve the app performance remarkably. But caching with the image component is supported in iOS alone, not in Android.
Here’s how you would cache an image:
<Image source={{ uri: 'https://unsplash.it/200/200?image=8' cache: 'only-if-cached' }} style={{ ... }} />
However, this method of caching is not optimal because it hardly solves the issues. Several issues, such as flickering, cache misses, poor app performance, and poor performance loading from the cache, may occur when the image is cached this way. It is possible to solve this issue with the react-native-fast-image
.
In addition to caching images, FastImage
also adds authorization headers and several other features:
import FastImage from 'react-native-fast-image' const App = () => ( <FastImage style={{ ... }} source={{ uri: 'https://unsplash.it/200/200?image=8', priority: FastImage.priority.normal, cache: FastImage.cacheControl.cacheOnly }} /> )
Let’s take a closer look at the properties of the FastImage
component. As the name suggests, the priority
props define the priority with which the images should be loaded. For an image needing to be loaded first, you would set the priority to FastImage.priority.high
.
Nevertheless, in the context of optimizing your React Native App, we are most interested in the cache
property. The cache
property allows us to cache images in three different ways:
FastImage.cacheControl.immutable
: This is the default value. The image will only be updated, if the URI changesFastImage.cacheControl.web
: The configuration from the source.headers
prop can be used for following normal caching procedures like an ordinary web browser would doFastImage.cacheControl.cacheOnly
: This is the most restrictive option. Only images from the cache will be shown and no network requests will be madeTogether with the image size, the image format may also affect the app performance. Developers coming from the web background would prefer JPEG
and JPG
formats because they allow for compression. However, this is not true for mobile platforms.
On the other hand, you could reduce the number of unique colors in each row of the pixels that make up a PNG
image. This could significantly reduce the image size. It is sufficient to say that the PNG
format is better than the JPG
format for mobile platforms.
The WebP
format introduced by Google in 2010 is the most performant of the three. It supports both lossless and lossy compression modes and can reduce the image size by up to 25-34%. Keep in mind that this format is not supported by all mobile devices. It is supported by Android 4.2.1 and higher devices and iOS 14.
InteractionManager
and LayoutAnimation
If not properly done, animations can affect the performance of your React Native application. The runAfterInteractions
method of InteractionManager
can be used to schedule long-running synchronous operations after an animation or interaction has been completed. This can improve the performance of a React Native application by ensuring that animations run smoothly:
InteractionManager.runAfterInteractions(() => { ... });
If you are concerned about the user experience, it may be preferable to use LayoutAnimation
instead. This would run the animation during the next layout:
import React, { useState } from "react"; import { LayoutAnimation } from "react-native"; if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } const App = () =>{ const [animate, setAnimate] = useState(false) const handleClick = () =>{ LayoutAnimation.configureNext(LayoutAnimation.Presets.spring) setAnimate(!animate) } return ( Button onPress={handleClick} title="Animate" /> ) }
The flag UIManager.setLayoutAnimationEnabledExperimental(true);
has to be set to ensure that it works on Android.
Running animation on the JavaScript thread is a bad idea. The JS thread can be easily blocked and this could make the animation run slowly or not run at all.
Because the Animated API is serializable, it is possible to push the details of the animation to native before the animation begins. As such, the native codes will perform the animation on the UI thread. This will ensure that the animation runs smoothly even if the JavaScript thread is blocked.
Here’s how to set useNativeDriver
with the Animated API:
import React, {useRef} from 'react' import {Animated} from 'react-native' const App = () =>{ const opacity = useRef(new Animated.value(0)).current const showVal = () =>{ Animated.timing(opacity, { toValue: 1, duration: 500, useNativeDriver: true, }).start(); } ... return ( <View> <Animated.View> <Animated.Text>Text to show</Animated.Text> </Animated.View> <Button title="Show" onPress={showVal} /> </View> ) }
Here, we declared a variable opacity
to hold the opacity value. We set the initial value to 0
and then we used Animated.timing
to trigger the animation on button click. useNativeDriver
is set to true
in Animated.timing
to send details of the animation to Native.
Each library in a React or React Native application leaves some footprint on the application. This is why you should only add libraries and features you need in your app and remove irrelevant dependencies and libraries.
Animations, navigations, tabs, and other features can contribute to the screen load time and so the more they are on the screen, the worse the performance.
Hermes is a JavaScript engine developed by Facebook in 2019. It is one of the must-have features for improving app performance, reducing memory usage, decreasing app size, and improving the app start-up time.
Hermes is not currently enabled by default in React Native, but you can easily enable it in your app.
Before the release of React Native v0.64, Hermes was available only for Android platforms, but it is now available for iOS as well.
If you are using an earlier version of React Native, you need to first upgrade it to the acceptable version before enabling Hermes.
To enable Hermes on Android, edit your android/app/build.gradle
file and add the following rules:
project.ext.react = [ entryFile: "index.js", enableHermes: true ]
You could also do it like this:
def enableHermes = project.ext.react.get("enableHermes", true);
If you are using ProGuard, open your proguard-rules.pro
and add the following rules:
-keep class com.facebook.hermes.unicode.** { *; } -keep class com.facebook.jni.** { *; }
Then clean and rebuild your app if you have already built it. To do this, first run cd android
and then ./gradlew clean
.
If you are upgrading your React Native version, be sure to edit the build.gradle
file accordingly with the latest Gradle releases.
Interestingly, Hermes for iOS was recently released in the new React Native version 0.64 on March 12, 2021. To enable Hermes for iOS, set hermes_enabled
to true
and run pod install
in your Podfile
:
use_react_native!( :path => config[:reactNativePath], :hermes_enabled => true )
Reselect
with ReduxLike the useMemo
hook, Reselect
can be used to create memoized selectors to optimize expensive computations. However, unlike useMemo
, this has to be used with Redux
.
Assuming we have a getPosts
state from our Redux store, we can use Reselect
to select posts with the highest likes and users from the posts.
Reselect
comes with the createSelector
function that can be used to create memoized selectors, as shown below:
import {createSelector} from "reselect" import _ from "lodash" //non-memoized selector export const getAllPosts = (state) => state.allPosts //memoized selector to get posts with top likes export const getMostLiked = (likes) => createSelector( getAllPost, item => item && _.filter(item, (post) => post.likes >= likes) ) //memoized selector to get users with the top likes export const getTopUsersByLikedPosts = (likes) => createSelector( getMostLiked(likes), item => item && _(item).map((post)=> post.user).uniqBy(post => post.id).value() )
First, we created a non-optimized selector getAllPosts
from our redux state allPosts
. We then created an optimized selector getMostLiked
on getAllPosts
and then filtered the items based on their likes. This memoizes getMostLiked
such that the values will be recomputed only when the posts data change.
Furthermore, we created a memoized selector, getTopUsersByLikedPosts
, from getMostLiked
and selected the users from the posts. This ensures that getTopUsersByLikedPosts
changes only when the top likes from the posts data change.
This is just an example. We could perform any expensive computation here and memoize it with Reselect
.
Reselect
should only be used when it is necessary; overusing it could even compound the performance issue in your React Native application.
Some processes and features could use up an unusual amount of memory than you would expect. Unless you want those processes, turning them off or optimizing them could be the key to improving your React Native performance.
You can monitor the memory usage on both Android studio and Xcode to find leaks that may be affecting the performance of your app.
This can be done through the Memory Profiler on Android Studio. You can open the Memory Profiler from View > Tool Windows > Profiler or locate the Profile icon from the toolbar.
You can also monitor the performance of your app from the developer menu in your app. To open the developer menu, use the shortcut Command + M (Mac) or Control + M (Windows and Linux). In the developer menu, toggle Show Perf Monitor
to monitor the performance of each component on your app.
Xcode also allows developers to find memory churns or leaks in their apps. You can find this from Product > Profile. Alternatively, you can use the shortcut Command + i to open the Leaks Profiler. Restart your app in your simulator to start identifying the memory leaks.
Memory leaks can be caused by several factors, as discussed above. Sometimes, cached images can clog up the memory and cause issues. In some cases, you may simply need to change ListView
to FlatList
to fix the issue. In any case, the Profiler will show you the actual cause of the problem.
The most popular library for handling navigation in React Native is the react-navigation
package. It is a normal JavaScript library which is also promoted by React Native itself on its documentation. Tips on how to install the package accordingly can be found here and here.
Let’s have a look at a simple implementation of react-navigation
:
import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import Home from "./screens/Home"; import Profile from "./screens/Profile"; const Stack = createNativeStackNavigator(); export default function App() { return ( <NavigationContainer> <Stack.Navigator initialRouteName="Home"> <Stack.Screen name="Home" component={Home} /> <Stack.Screen name="Profile" component={Profile} /> </Stack.Navigator> </NavigationContainer> ); }
In the App.js
, we wrap our components in a NavigationContainer
. Secondly, we created a Stack Navigator, which is the most common way of navigation in mobile apps. The default route is defined in line 9 and in our case, it’s the Home screen:
import { View, Text, StyleSheet, Button } from "react-native"; import React from "react"; export default function Home({ navigation }) { return ( <View style={styles.container}> <Text>Home Screen</Text> <Button title="Go to Profiule Page" onPress={() => navigation.navigate("Profile", { name: "John Doe", age: 25, email: "[email protected]", phone: "123-456-7890", }) } style={styles.button} /> </View> ); }
In the Home.js
file we add a button, which will allow us to navigate to the profile screen. Please note, in line 10, that we can easily pass data between the screens:
import { View, Text, StyleSheet, Button } from "react-native"; import React from "react"; export default function Profile({ route, navigation }) { const { name, age, email, phone } = route.params; return ( <View style={styles.container}> <Text>Profile Screen</Text> <Button title="Go Back" onPress={() => navigation.goBack()} style={styles.button} /> <View> <Text>Name: {name}</Text> <Text>Age: {age}</Text> <Text>Email: {email}</Text> <Text>Phone: {phone}</Text> </View> </View> ); }
Inside the Profile.js
, we then render the information that got passed to it. In line 10, we see another possibility to navigate between screens. In the Home.js
, we explicitly set the profile screen to be the destination, but here we just say navigation.goBack()
.
While it is pretty straightforward to get started with this library, it’s smart to watch out for different navigation libraries for increasing the app performance. Especially on Android devices it can come to performance decreases when your apps get more complex. Also, the overhead of this navigation option is quite high. @react-navigation/native
, @react-navigation/native-stack
, react-native-safe-area-context
and react-native-screens
will end up in your project’s package.json
.
For a more performant and lightweight alternative, you can try out the react-native-navigation
library, which is tied to the native components. Even though the installation and the docs generally are not quite as intuitive when compared to the first navigation library, it is still worth it. Check out this page for the installation part. Below you can find the code for a simple demo for how to navigate between two screens:
import React from 'react'; import { View, Text, Button, StyleSheet } from 'react-native'; import { Navigation } from 'react-native-navigation'; const HomeScreen = (props) => { return ( <View style={styles.root}> <Text>Home</Text> <Button title='Go to profile' onPress={() => Navigation.push(props.componentId, { component: { name: 'Profile', options: { topBar: { title: { text: 'Profile' } } } } })}/> </View> ); }; const ProfileScreen = () => { return ( <View style={styles.root}> <Text>Profile Screen</Text> </View> ); } Navigation.registerComponent('Home', () => HomeScreen); Navigation.registerComponent('Profile', () => ProfileScreen); Navigation.events().registerAppLaunchedListener(async () => { Navigation.setRoot({ root: { stack: { children: [ { component: { name: 'Home' } } ] } } }); }); const styles = StyleSheet.create({ root: { flex: 1, alignItems: 'center', justifyContent: 'center', } });
There are still several other libraries for handling the navigation in React Native, but those two are the most popular options.
Performance is a crucial factor in every React Native application, but it is also a complex topic.
Several factors can affect performance, ranging from console statements and animations to large-sized images and heavy computations. It is important to identify the sources of these memory leaks and poor performance and fix them.
Interestingly, both Android Studio and Xcode provide us a way to monitor memory usage. We also have a plethora of tools and methods to optimize React Native applications for performance, as discussed. Try out these optimization techniques in your next React Native project.
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
2 Replies to "Optimizing React Native performance"
Awesome explantion for the dev’s
Thanks for the high quality article