Ifeanyi Dike As a full-stack software engineer, I specialize in React, React Native, and Node.js. I'm currently a Team Lead at Sterling Digitals Limited. I spend most of my time building apps or writing blogs, and my leisure time is spent playing chess.

Optimize your React Native app performance

11 min read 3213

Optimize React Native Performance

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.

How does React Native Work?

A 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 and large 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.

Use FlatList or SectionList to render large lists in React Native

If 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 application 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.

Remove all console statements

Console statements are necessary for debugging JavaScript codes, but they are meant for development purposes alone. These statements could cause serious performance issues in your React Native application if they are not removed before bundling.

We made a custom demo for .
No really. Click here to check it out.

While you could install some 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.

Memoize expensive computations

React introduced the memo HOC (Higher Order Component) in React 16.6 for preventing unnecessary re-rendering and the useMemo hook in React 16.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.

The React.memo Higher Order Component (HOC)

React.memo was introduced to functional components to serve the same purpose the React PureComponents serves in class components. memo prevents unnecessary re-rendering of a component and could help to optimize an application.

However, like other optimization techniques, memo should only be used when it is necessary. In some cases, unnecessary re-rendering would not impact so much on performance.

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 some performance issues.

The useMemo Hook

useMemo returns a memoized value of a function. However, it should be used only 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 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.

The useCallback Hook

The 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.

Adjust (resize and scale down) image sizes

Images can contribute significantly to performance issues in React Native applications. They can also pose some 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 a lot of 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.

Cache images locally

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, Fast Image 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',
            headers: { Authorization: 'auth-token' },
            priority: FastImage.priority.normal,
        }}
        resizeMode={FastImage.resizeMode.contain}
    />
)

You could also use some other libraries such as the react-native-cached-image to cache and load images. Here’s how to use it.

import { CachedImage } from 'react-native-cached-image';

<CachedImage 
  style={{ ...  }}
  source={{ uri: item.urls.raw }}
/>

Use fast loading image formats in React Native

Together 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.

Schedule animations with 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 could improve the performance of a React Native application by ensuring that animations will 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.

Use native driver with the Animated API

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.

Remove unnecessary libraries and features

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.

Use Hermes

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 0.64, Hermes was available only for Android platforms but with the recent release, 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 has recently been 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
)

Use Reselect with Redux

Like 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.

Monitor memory usage in React Native

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 the performance of your React Native application.

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 could 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.

Conclusion

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. You could try out these optimization techniques in your next React Native project.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Ifeanyi Dike As a full-stack software engineer, I specialize in React, React Native, and Node.js. I'm currently a Team Lead at Sterling Digitals Limited. I spend most of my time building apps or writing blogs, and my leisure time is spent playing chess.

One Reply to “Optimize your React Native app performance”

Leave a Reply