Share on Twitter
Share on Facebook
Share on HackerNews
Share on LinkedIn

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.

Your code is broken. Let's Fix it.
Get Started

More from the Sentry blog

ChangelogCodecovDashboardsDiscoverDogfooding ChroniclesEcosystemError MonitoringEventsGuest PostsMobileOpen SourcePerformance MonitoringRelease HealthResourceSDK UpdatesSentry
© 2024 • Sentry is a registered Trademark
of Functional Software, Inc.