10 Tips for Optimizing React Native Application Performance: Part 1
React Native is an amazing framework for building cross-platform mobile applications. It helps you provide a high-quality, native-looking application that runs on multiple platforms, using a single codebase.
The current React Native architecture uses the UI (or main) thread and the JavaScript thread. The JavaScript thread is used to run business logic, make API calls, process touch events, etc. The UI thread is where animations and transitions are done. When an app’s performance is compromised, the JS thread can become unresponsive, leading the UI thread to drop animation frames. This in turn can produce visible jitters or flickers in your app’s UI, leading to a poor user experience.
Although React Native is a framework that is developed with performance in mind, there still can be some areas where an application's performance can be compromised. In this article, we will discuss common pitfalls to avoid and offer some tips on optimizing your React Native application for performance before shipping to production.
Remove console statements before releasing the app
Developers commonly use console.*
statements for debugging purposes, but leaving them in a production-released application can cause a bottleneck.babel-plugin-transform-remove-console
will automatically remove all console.*
statements before bundling a production release of the app. To use it, add it as a dev dependency and then add the following to your .babelrc
file:
{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}
Use FlatList to render large arrays of items
One common way to display a list of items in React Native is to use a ScrollView
component, which traverses the items from an array using the JavaScript map
function.
For example:
<ScrollView>
{items.map(item => {
return (
<View key={item.id.toString()}>
<Text>{item.name}</Text>
</View>
);
})}
</ScrollView>
Using the ScrollView
component is fine for an array with a small number of items, but can lead to performance problems once that array grows large, because it renders all of the items up front. In these cases, the FlatList
component is a better choice, because it lazy loads the list items, as they appear on the screen. Further, it deletes them when they are no longer displayed, which allows the component array to have a smaller and more consistent memory footprint.
Converting the previous example to use FlatList
looks like this:
// listItems is an array
function ExampleScreen() {
const renderListItem = () => {
return (
<View>
<Text>{item.name}</Text>
</View>
);
};
return (
<FlatList
data={listItems}
keyExtractor={item => item.id}
renderItem={renderListItem}
/>
);
}
In the above snippet, the renderItem
prop on the FlatList
component takes a callback function, called renderListItem
. The FlatList component has a variety of additional props that you use to further optimize performance when rendering large lists. For example, you can use the getItemLayout
prop when you know ahead of time that each item in the list has a fixed size (height or width). This optimizes the rendering by skipping the individual measurement of each list item and creates a noticeable performance boost for lists containing several hundred items.
Use nativeDriver with Animated API
Animated API is a React Native core module that enables the rendering of animations in an application, calculating each frame on-demand in the JavaScript thread. As a result of this just-in-time calculation, a complex animation can begin to drop frames when the JS thread becomes blocked by another complex computation, leading to the jitteriness and flicker mentioned above.
Fortunately, the Animated API can use nativeDriver
to send animations to the main thread before the animation starts. This way, an animation executes independently of a blocked JavaScript thread and results in a smooth user experience with no dropped frames. To use nativeDriver
with Animated, set useNativeDriver
to true
.
In the example below, nativeDriver
is used on an onScroll
Animated event on a ScrollView
component.
<Animated.ScrollView
scrollEventThrottle={1}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }],
{ useNativeDriver: true }
)}
>
{content}
</Animated.ScrollView>
Use a cache to optimize images
It is good practice to cache images, to avoid unnecessary network requests. Caching an image will save the image locally for the first time it loads in the application. For subsequent requests, the local cache is used to serve the image. Avoiding subsequent network requests by using a cache significantly improves the application's performance.
The React Native Image
component uses a prop called cache
to allow caching of images. Here is an example:
<Image
source={{
uri: 'https://reactjs.org/logo-og.png',
cache: 'only-if-cached',
}}
style={{ width: 400, height: 400 }}
/>
Built-in caching is available for iOS only, however, and doesn't resolve other issues like slow loading from the cache, cache misses, and flickering. Using a third-party library like react-native-fast-image to handle caching can help you address these issues.
Use an appropriate image size
There are other React Native optimization techniques, in addition to caching, that you use when handling a large number of images. Because displaying a large list of images or rendering many images at once on an app screen leads to high memory usage, it’s best to reduce the memory cost of each individual image as much as possible. One way to do this is to create scaled-down versions of your images where appropriate, rather than scaling them at runtime. For example, create a separate thumbnail-sized version of each image in a gallery, and only display the full-size image once zoomed in.
Use an appropriate image format
Another way to optimize React Native application performance when loading a large set of images is to use the appropriate image format. PNGs, while they offer transparency and greater image fidelity, most often take up more space than equivalent JPGs, which trade compression efficiency for the possibility of small amounts of image degradation. Therefore, it’s best to only use PNGs in places where distortion or blurriness would be very obvious, such as in graphic design elements like logos. For photos and other images where crispness isn’t as important, use JPG. JPG is the most popular image format in the web world because of the efficient compression technique. However, using PNG format for images using the same color is compressed more efficiently.
A new format, called WebP, offers the best of both worlds - more efficient compression than either PNG or JPG, without loss of data. With space savings of 25% or more, it’s a tempting format to use. Keep in mind, however, that WebP images are only supported on iOS 14 and above and Android 4.2.1
and above.
Enable Hermes for both iOS and Android
Hermes is a JavaScript engine, developed by Facebook, which is optimized for React Native. It provides app performance improvements, reduced memory usage, decreased app size, and improved app start-up time, and is available as an opt-in feature for all React Native apps using React Native 0.64.x
or higher. To enable Hermes on iOS, set hermes_enabled
to true
in your ios/Podfile
:
use_react_native!(
:path => config[:reactNativePath],
# to enable hermes on iOS, change `false` to `true` and then install pods
:hermes_enabled => true
)
Then, navigate to the ios/
directory in the terminal window and run the command pod install
to install the Hermes pod. After the pod installation is complete your app is rebuilt, you can test it out using the command npx react-native run-ios
.
For Android, set enableHermes
to true
in your android/app/build.gradle
file:
project.ext.react = [
entryFile: "index.js",
// change the line below to true
enableHermes: true
]
You can then run the command npx react-native run-android
to rebuild and develop your app.
Avoid anonymous functions
Inline anonymous functions are very common in JavaScript, but can cause serious performance issues when used in React components. In the following example, any time the Button
component is re-rendered, a new copy of the onPress
callback needs to be created.
<Button
onPress={() => setCount(count + 1)}
title="Click me!"
/>
Using a named function solves this problem. In this version, handlePress
only needs to be created once.
const handlePress = () => setCount(count + 1)
<Button
onPress={handlePress}
title="Click me!"
/>
Use React.memo() to avoid unnecessary re-renders
React introduced the concept of memoization in version 16.6
, with a higher-order component (HOC) called React.Memo
. When the function component is wrapped with React.Memo
receives the same set of props more than once, React will use previously cached versions of those props and will render the JSX returned by the function component only once.
For example, consider the following App
and Button
components. The App
component has state variables called count
and secondCount
, which are updated when the button is pressed.
// App component
import React, { useState } from 'react';
import { View } from 'react-native';
import Button from './Buttonc';
const App = () => {
const [count, setCount] = useState(1);
const [secondCount, setSecondCount] = useState(1);
return (
<View>
<Button setValue={setCount} value={count} label="Increase First" />
<Button
setValue={setSecondCount}
value={secondCount}
label="Increase Second"
/>
</View>
);
};
// Button component
import React from 'react';
import { View, Text, Pressable } from 'react-native';
const Button = ({ label, value, setValue }) => {
const handleOperation = () => {
setValue(value + 1);
};
return <Pressable onPress={handleOperation} />;
};
export default Button;
Whenever either button is pressed, both buttons get re-rendered, even though only one of count
and secondCount
has changed value. If rendering each button involved some expensive computations, re-rendering both the buttons every time one of them is pressed could cause a performance issue.
To prevent this behavior, wrap the Button
component with the React.memo()
HOC.
// Button component
export default React.memo(Button);
Use useMemo and useCallback hooks to avoid unnecessary re-renders
In version 16.8
, React introduced two new hooks: useMemo
and useCallback
. While React.memo()
is a HOC and therefore wraps other components, the hooks act directly on functions and can be used to wrap any function that contains expensive computations. Taking the previous example of the Button
component, the memoization can be applied to the handleOperation
function instead of the whole component, since that is where the calculation is happening.
// Button component
import React, useMemo from 'react';
import { View, Text, Pressable } from 'react-native';
const Button = ({ label, value, setValue }) => {
const handleOperation = useMemo(() => {
setValue(value + 1);
}, [value]);
return <Pressable onPress={handleOperation} />;
};
export default Button;
useMemo
and useCallback
behave similarly. The difference between the two is that the useMemo
hook returns the memoized return value of the function that is passed to it, while the useCallback
hook returns a memoized callback.
Conclusion
Performance is a crucial aspect of any application, but it’s a complex topic, and poor performance can have a number of different causes. Keeping the tips above in mind will help you continue to develop and deliver a seamless experience for your React Native app users.