React Native Performance: Do and Don’t
React Native is a cross-platform JavaScript library that creates high-performance apps by default. However, there are a few tricks you can use to optimize your React Native performance.
6 October 2021
Performance is a topic with major ramifications for using a framework like React Native in real-world mobile applications. Simply put, React Native is fast by default. While working on a React Native app, however, you might experience performance issues.
Do not assume these issues can be fixed by testing components. In this post, we’ll offer a list of suggestions for optimizing performance while building a React Native app.
React Native offers an Image component as part of its core set of components. This component is used to display an image, but, out of the box, it does not have a solution for issues like:
The Image component in React Native handles caching images like web browsers, which is sometimes the cause of the above issues. They are easily resolved by using a third-party library called react-native-fast-image. It is available for both iOS and Android and is efficient at caching images.
Optimizing an image is important for a React Native app’s performance if the app relies on using a huge amount of images. Rendering lots of images could lead to high memory usage on a device if the images are not appropriately optimized in terms of size. This may cause the app to crash.
Some things that can be done to optimize images in a React Native app include:
React Native is based on the React library and handles rendering components in a similar way to React.js. Therefore, the optimization techniques that are valid with React also apply to React Native applications. One optimization technique is to avoid unnecessary renders, and in functional components, this can be done by using React.memo()
.
React.memo()
is used to handle memoization. The concept of memoization is described as follows: if a component receives the same set of props more than once, it will use previously cached props and render the JSX returned by the functional component only once.
For example, consider the following parent and a child component. The Parent
component has a state variable called count
that is updated when the button is pressed.
Whenever the button is pressed, the Child
component also gets re-rendered even though its prop text
does not change on each render. It is not doing anything special to its parent component and is just displaying some text. This can be optimized by wrapping the contents of the Child
component with React.memo()
.
// Parent.js
const Parent = () => {
const [count, setCount] = useState(0);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title='Press me' onPress={() => setCount(count + 1)} />
<Child text='Placeholder text' />
</View>
);
};
// Child.js
const Child = React.Memo(({ text }) => {
return <Text>{text}</Text>;
});
There are many ways to create animations in a React Native app. One of the most popular ways to do this is to use the Animated library.
Animated uses nativeDriver
to send animations over the native bridge before animation starts. This helps the animations to execute independently of a blocked JavaScript thread, thus resulting in a smoother experience without dropping many frames.
To use nativeDriver
with the Animated library, you can set its value to true
. In the example below, useNativeDriver
is used on an onScroll
Animated event in a ScrollView
component.
<ScrollView
showsVerticalScrollIndicator={false}
scrollEventThrottle={1}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: animatedValue } } }],
{ useNativeDriver: false }
)}
>
// Component's content
</ScrollView>
React Native version 0.62.0
introduced a new tool called Flipper. It’s a debugging platform for iOS, Android, and React Native apps. It integrates directly with the native code, and its integration with a React Native app is enabled out of the box.
Using Flipper to debug apps does not require remote debugging. It requires a locally connected instance of Metro to interact with the React Native app. It has React DevTools to inspect the component tree and check out the state and props of a React component.
It uses a native plugin ecosystem for debugging both iOS and Android applications. These plugins are used for device logs, crash reports, inspecting network requests, inspecting the local database of an app, inspecting cached images, etc.
Hermes is an open-source JavaScript engine optimized for mobile applications. Since React Native version 0.60.4
, Hermes has been available for the Android platform. It helps with reducing the download size of an app (APK for Android), reduces memory consumption, and reduces the time it takes for an app to become usable (TTI – Time to Interact).
To enable the Hermes engine in an Android app, open build.gradle
and modify the following:
def enableHermes = project.ext.react.get("enableHermes", true);
Since React Native version 0.64-rc.0
, Hermes is also available to be used on the iOS platform. To enable it for iOS, open Podfile
and modify the following code:
use_react_native!(:path => config[:reactNativePath], :hermes_enabled => true
Using console.log
statements is one of the most common methods to debug in JavaScript applications in general, as well as in React Native apps. However, leaving the console statements in the source code when building a React Native app for a platform could cause some big bottlenecks in the JavaScript thread.
One way to keep track of console statements and remove them is to use a third-party package called babel-plugin-transform-remove-console
. To use it, install the package by using the following command in a terminal window:
yarn add babel-plugin-transform-remove-console
Then, modify the .babelrc
file to remove all console statements:
{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}
There are few ways to create scrollable lists in React Native. Two of the common ways available in the React Native core are ScrollView
and FlatList
components.
A ScrollView
component is simple to implement. It is often used to traverse over a list of finite number of items using a JavaScript’s map()
function. For example:
<ScrollView>
{items.map(item => {
return <Item key={item.id.toString()} />;
})}
</ScrollView>
The ScrollView
component renders all children at once. This is good for use cases where the number of items in a list to render is quite low. Dealing with a large amount of data can directly affect the performance of the app.
To deal with large lists of items, React Native provides a component called FlatList
. This component ensures that the items are lazy loaded such that the app does not consume an inconsistent amount of memory.
For example:
<FlatList
data={elements}
keyExtractor={item => `${items.id}`}
renderItem={({ item }) => <Item key={item.id.toString()} />}
/>
React Native is an open-source framework used to create cross-platform mobile applications. It uses JavaScript at its core and has a primitive API of components to build mobile interfaces and functionalities. It’s a high-performance framework as long as you build with performance in mind from the start.
At Crowdbotics, the React Native framework is an integral part of the RAD stack. We use React Native (as well as React Native Web and React Native for Windows + MacOS) to build universal applications from a single codebase, and the Crowdbotics App Builder automatically scaffolds apps with prebuilt React Native screens.
If you’re interested in building a custom mobile app completely from scratch, Crowdbotics provides expert PMs and developers to manage your app build. Get in touch with us today for a detailed quote and development timeline.