06Aug
Performance Optimizations for React Native Applications
Performance Optimizations for React Native Applications

Introduction

React native has grown to be one of the most popular frameworks for building cross-platform mobile applications. React native allows us to share about 90% of our codebase between multiple platforms while maintaining a native feel and aesthetics. It is also good in terms of performance but sometimes you might run in some performance bottlenecks that need troubleshooting and fixing. So in this article, we will be talking about some performance issues on react native and suggested solutions.

This article will talk a bit about the architecture of react native so we can understand where some performance issues can arise, then discuss some performance issues and tips to fixing them.  This article assumes that you have some knowledge of React Native and have used it to build a mobile application.

Under the hood

In other, for us to understand how some performance issues come up we have to understand a bit about how react-native works under the hood.

There are three main parts that form the react-native internals:

  1. Native/Main thread: This is the native thread that runs on iOS and Android phones, in Android it can be in Java or Kotlin, and in iOS it can be in objective or Swift. This thread is that displays the view/user-interface and handling user gestures.
  2. Javascript Thread: This is the react-native part of the application. This thread deals with the business logic and functionality of the application, this in most cases is shared by both platforms.
  3. Bridge: This thread allows bidirectional and asynchronous communications between these two universes, basically the native thread will need arguments and the current application context to  do its work so what the bridge does to asynchronously serialized (stringified to JSON) the arguments and context then pass it between the two threads
React native architecture diagram
React native architecture diagram

So when you open a react native application the first thing that happens is that the native thread is loaded then the native thread starts the Javascript engine(JScore) which runs the react-native code(business logic). The native thread then sends a message through react-native bridge to the JS thread to start the application. Now the application has started to the Javascript can issue instructions to the native thread via the react-native bridge.

The number of calls/instructions that need to cross the bridge are not definite and can vary based on the application and user interaction, and it can take time to serialize all those individual JSONs before they are passed to the other thread. If another call comes in while the bridge is still processing data it will have to be blocked until the current data is processed which might lead to dropped frames and janky animations(which can give a sense of poor performance). This makes it important for us to limit what is being sent over the Bridge to keep communication fast between the two realms.

Now that we have discussed the basic architecture of react native apps and how we can easily run into performance issues lets discuss some other issues that might indicate a performance problem.

Performance Bottleneck Indicators and How to fix them

These are some indicators of poor performance in a react native application. How do we fix them? we address each of these bottlenecks and exploring different ways we can fix these issues.

Janky Frames

Janky frames or slow animations make the application feel slow and buggy. React native promises that is offers 60 fps, which means it tries to show 60 frames of a picture per second so make it seem as real as possible. What this means is that the Main thread and the javascript thread have about 16.67ms to generate the frame that user sees in that time period. If this is cannot happen in 16.67ms then a frame is dropped and the user interface can appear unresponsive or janky.

  • Remove console.log() statements:  I know a good number of developers(I am them) who us console.logs when debugging, This is not a bad thing but it is important we don’t forget them in the codebase because according to the react-native documentation console.log can cause a bottleneck in the Javascript thread.
    Also libraries like redux-logger have a known performance issue in react native because it probably does a lot of logging internally. So it is important to remove this console.* calls before bundling our react native app. There is a babel plugin that can handle this for us, babel-plugin-transform-remove-console removes all the console calls when the app is being bundled, just install it using npm i babel-plugin-transform-remove-console –save-dev or yarn add babel-plugin-transform-remove-console then in your .babelrc file add the config below.

    {
      "env": {
        "production": {
          "plugins": ["transform-remove-console"]
        }
      }
    }

    Or you can add this function at the top of your app.js/or app entry point.

    if (!__DEV__) {
      console.log = () => {};
      console.info = () => {};
      console.warn = () => {};
      console.error = () => {};
      console.debug = () => {};
    }
  • Use InteractionManager: To ensure animations are performed at 60FPS react-native provides us with the InteractionManager. InteractionManager allows us to schedule an expensive javascript task after any interaction/animations have been completed.
    InteractionManager.runAfterInteractions(() => {
      
    });

    This is particularly useful in React navigation when you are navigating to screen that does some heavy computation to render the UI and as a result of the computation, the navigation animation may flicker or jerk. What happens here is that when the new screen is being navigated to, React navigation renders it in the background then animates it into view. This means that logic in the componentDidMount or any similar logic will start executing while the navigation animation is still in transition and this might lead to dropped frames because the Javascript animations and business logic are both running on the Javascript thread.
    We can solve this using InteractionManager and a bit of UX. One of InteractionManager api’s runAfterInteractions will help us delay business logic from executing until the navigation animation completes.

    import React, { useState, useRef, useEffect } from 'react';
    import {
      Text,
    
      InteractionManager,
    
    } from 'react-native';
    
    export default function App() {
      const [screenLoading, setScreenLoading] = useState(true);
      useEffect(()=>{
        InteractionManager.runAfterInteractions(() => {
          // 2: Component is done animating 
          // 3: Start fetching data that is needed to render UI
          setScreenLoading(false) //Set screenloading prop to false
       });
      },[])
      return (
        //render skeleton placeholder/any placeholder when screenLoading is true
        //then
        //render appropirate UI based on heavy computation when the data is been successfully fetched from server
      );
    }

    From the above example, we use interactionManager to give priority to the navigation animation when that has completed, we can display a placeholder(loader or skeleton placeholder)/ cache data until the data/heavy UI rendering is done then we can show the real UI.

  • Use Flatlists for rendering list: As a Reactjs developer who learned react-native after it is easy to assume the way to display a list of items(images, text e.t.c) from an array is to map through the array using Primitive elements(View,Text) inside a scrollView .
    import * as React from 'react';
    import { Text, View, StyleSheet, ScrollView, FlatList } from 'react-native';
    
    
    const veryLargeUsersList =[
      {id:1,name: 'joe', age: 18},
      {id:2,name: 'zero', age: 24},
      {id:2,name: 'zero', age: 24},
      {id:2,name: 'zero', age: 24},
      .......
    ]
    
    export default function UserDetails() {
      return (
        <View style={styles.container}>
         <ScrollView>
          {
            veryLongUsersList.map( user =>(
                <View>
                  <Text>{user.name}</Text>
                  <Text>{user.age}</Text>
                </View>
            ))
          }
         </ScrollView>
        </View>
      );
    }

    But this has implications in terms of performance and other optimizations because this is not optimized for large data and infinite scrolling. So ideally we should be using Flatlists for rendering lists because it handles all the performance issues internally.

    import * as React from 'react';
    import { Text, View, StyleSheet, ScrollView, FlatList } from 'react-native';
    
    
    const veryLargeUsersList =[
      {id:1,name: 'joe', age: 18},
      {id:2,name: 'zero', age: 24},
      {id:2,name: 'zero', age: 24},
      {id:2,name: 'zero', age: 24},
      .......
    ]
    
    
    export default function UserDetails1() {
      return (
        <View style={styles.container}>
         <FlatList
          data={veryLongUsersList}
          keyExtractor={({ id }) => id}
          renderItem={(user)=>(
            <View>
                  <Text>{user.name}</Text>
                  <Text>{user.age}</Text>
                </View>
          )}
         />
          
        </View>
      );
    }

Unnecessary re-renders

This mostly happens on the javascript thread, Inefficient/useless state updates that can lead to unnecessary repainting of the screen which in turn overuse the already limited resources in the bridge. React native triggers a render/re-render when a component parent is re-rendered or the props/state change so it is necessary to structure it in a way that minimizes renders.

  • Structure component properly: It is important to structure your application tree in a way that components only receive props they need to render if this is combined correctly with the use of PureComponents you should get desirable results.
    It is very easy to fall into the trap of using PureComponent or Usememo when trying to optimize for re-renders without understanding what it does. PureComponent internally implements shouldComponentUpdate() which does a shallow comparison of the previous and current component’s state and props. This looks like a good thing but according to the ReactJs docs PureComponent’s shouldComponentUpdate() may lead to false-negative when comparing complex and deeply nested data structures.
  • Use uncontrolled inputs:  Contrary to practices in React where most of my input fields are controlled input(inputs are controlled by the component’s state), it is more performant to use uncontrolled input in React native.
    import React, { Component } from 'react';
    import { TextInput } from 'react-native';
    
    export default function ControlledInput() {
      const [text, onTextChange] = React.useState('controlled input');
    
      return (
        <TextInput
          style={{  borderWidth: 2, height: 60, borderColor: 'black' }}
          onChangeText={text => onTextChange(text)}
          value={text}
        />
      );
    }

    The problem with controlled inputs is that on slower devices or when a user is typing really fast the might be problem with updating the view. Basically controlled input deals have to cross the RN bridge because it deals with both the native(displaying the Input) and javascript thread(updating component state and any other parsing/validation). Due to the known performance limitations of the RN bridge using uncontrolled is the most accessible approach, this is as simple as removing the value prop also we don’t have to deal with re-renders because there is no state change when modifying an uncontrolled input.

    export default function UncontrolledInput() {
      const [text, onTextChange] = React.useState('controlled input');
    
      return (
        <TextInput
          style={{  borderWidth: 2, height: 60, borderColor: 'black' }}
          onChangeText={text => onTextChange(text)}
          defaultValue={text}
        />
      );
    }
  • Stop passing Inline functions as props: When passing a function as a prop to component avoid passing that function inline like the below
    function MakeCoffee(props){
      return(
        <Button title=' Make Coffee' onPress={props.onPress}/>
      
      )
    }
    
    
    export default function CoffeeMaker() {
     
      return (
        <View style={styles.container}>
          <MakeCoffee
            onPress={()=> console.log('making some coffee')}
          />
        </View>
      );
    }

    The above isn’t good because anytime the parent re-renders a new reference of that function is created so the child component re-render even when the props didn’t change. The advice is to declare the function as a class method in a class component or as a function inside a functional component so the reference removes the same across re-renders like the below.

    export default function CoffeeMaker() {
      function handleMakeCoffee(){
        console.log('making coffee please waitt')
      }
      return (
        <View style={styles.container}>
          <MakeCoffee
            onPress={handleMakeCoffee}
          />
        </View>
      );
    }

Slow Load time

This is  the amount of time it takes the application to load, this is a performance metric also because there is a chance users might drop your app if it appears unresponsive on startup.

  • Inline requires: When a RN app is starts up it has to loads all the javascript code into memory and then parses it, for a standard react native app that is about 50mb and this is a lot to load on startup. So what inline requires do is to defer requiring an expensive module until that file is needed. Inline Requires was introduced in RN 0.59, what it does is to analyze your import statements and lazy loads them when they are required. This is quite similar to what webpack’s tree-shaking does on the web. This is not enabled by default on Bare React native application and Expo application so we have to enable it.
    For Bare React native application a metro.config.js file was introduced in RN 0.59, here is where we set the InlineRequires property to true to enable it

    module.exports = {
      transformer: {
        getTransformOptions: async () => ({
          transform: {
            experimentalImportSupport: false,
            inlineRequires: true,
          },
        }),
      },
    }

    For Expo applications this was introduced in SDK 35, a lazyImports option was added to babel-preset-expo which allow us to enable it.

    module.exports = function(api) {
      api.cache(true);
      return {
        presets: [['babel-preset-expo', { lazyImports: true }]],
      };
    };
  • Enable Hermes: Hermes is a javascript engine that is optimized for React native apps on the android platform. Enabling Hermes on your react native project can lead to better start time, smaller bundle size, and lower memory usage on android devices. Hermes was released in 2019 and has been available in React 0.60.4> but right now it is only supported on android but there is a plan to support iOS.
    To enable Hermes go to your android/app/build.gradle then set the enableHermes to true.

    project.ext.react = [
      entryFile: 'index.js',
      enableHermes: true
    ]

    After enabling Hermes just rebuild your application and start enjoying performance benefits. Note that enabling Hermes has no implications for your iOS application, it just continues using the Javascriptcore engine that is available to iOS devices. You can read more about Hermes.

Benchmarking

I think it is also important to avoid premature optimization by measuring before you start optimizing for performance. So below I will discuss some tools in react native we can use for benchmarking RN apps.

  • React native performance monitor: This is an inbuilt performance monitor that comes with react native, you can access it by opening the developer menu and selecting `Show Perf Monitor`. This shows you the current frame rate, GPU usage e.t.c.
  • React profiler: This is a more comprehensive performance monitoring tool for react and react native, it shows you the whole application tree, props, and state of each component and a flame chart to see components that re-render. This is a very useful tool for performance profiling.
  • Why did you render: This is a useful nom package that helps you know when and why a component re-render.
  • Firebase performance monitoring: This is performance monitoring of your app in production, this can be used to track performance on the end-user devices.

Conclusion

It is easy to fall into the trap of premature optimization so the best tips I have gotten when trying to optimization any application is to Measure, Fix, and then Measure again.

References

One Reply to “Performance Optimizations for React Native Applications”

  1. Muhammad 4 years ago

    Thanks for the information will try all the steps
    here is a typing mistake “nom package” near :Why did you render:

Leave a Reply