Kirill Golubev

Resent Posts

Categories

Tags

IMG_20170504_112937_922

Kirill Golubev

Front-End / Mobile App Developer

Performance optimization in React Native applications

React Native offers great performance out of box, especially compared to the Hybrid frameworks using Web technologies, such as Cordova, or Ionic. React Native has the asynchronous nature and runs the JavaScript thread separately from the UI thread. All operations are asynchronous and non-blocking, which has a positive effect on user’s experience and app’s responsiveness. The framework is using the so-called bridge and sends messages triggering rendering or updates of native views through this bridge, only if it is necessary.

A diagram showing how React Native works, with its multiple threads

React Native’s multi-threaded architecture (Source https://calendar.perfplanet.com/2018/react-native-sync-async/)

However, even though in most cases the React Native performance is good or acceptable, in some cases the performance can suffer and there are definitely some techniques we can use to improve it. In some cases, this improvement will be quite substantial. In this article, I summarized different techniques to improve React Native app performance.

It should be noted, that app’s performance should be measured in the release mode because of the optimizations React Native applying in this mode. Also, you should remember that some of the optimizations, such as using the Hermes VM, will only work for one specific platform, or only starting from certain RN versions. Another thing to consider, that in some scenarios the application of some of the techniques from this article can have the opposite effect, so you should test your app thoroughly before and after application of any of the optimizations from this article.

Use the production build (Android and iOS)

When you are measuring and testing your RN app performance, make sure you are running it in the release (production) mode. In this mode React Native will apply additional optimizations and will disable some features related to debugging and logging. It will also reduce the bundle size.

All these changes will give you visible performance boost and might solve the issues you were experiencing in the development mode. In other words, before investing your time in your app performance optimization make sure they are present in your release build. Only after confirming that your release build performance suffers as well, start your quest for improving your app performance.

You can find instructions on how to run your React Native app in the release mode for both Android and iOS in my next article.

Hermes VM (Android only)

React Native team found out that one the bottlenecks on Android platform is this platform JavaScript engine. To remove this bottleneck they decided to build their own JavaScript engine, that they called Hermes. Hermes VM is available from React Native 0.60.4 and can be enabled by editing android/build.gradle file and setting enableHermes to true

In case you are using ProGuard library for Android, you will also need to add the following line to proguard-rules.pro file:

            -keep class com.facebook.hermes.unicode.** { *; }

Also, if you built your app at least once, make sure you clean your build manually, or in the terminal by running the command:

            cd android && ./gradlew clean

The Hermes JVM is still experimental and there are many reports that it can be unstable, so use it with caution, and test your Android build thoroughly, especially in the release mode to avoid app crashes. It was also noticed by me and other React Native developers that the Hermes engine is noticeably slower in the development mode. Though, this is not the case for the release mode.

Avoid using console.logs in production (Android and iOS)

All calls to console.log in the release mode can be removed automatically by installing

babel-plugin-transform-remove-console and editing your .babelrc file.

First install the library by running the following commands:

npm install babel-plugin-transform-remove-console –save-dev

or in case you are using yarn, you will need to run:

yarn add babel-plugin-transform-remove-console

After that, you will need to add the to your .babelrc file:

{

  “plugins”: [“transform-remove-console”]

}

For more information please visit the official npm repo of the library here link.

Avoid deprecated components (Android and iOS)

Avoid using deprecated components if there are alternatives. In most cases, the alternatives will provide better performance than their deprecated counterparts and what is even more important, one day your app will not crash when the deprecated component is eventually removed from the RN core. The examples of the deprecated components and their newer alternatives from React Native core are the following:

Deprecated component Alternative (RN 0.60.4)
ListView FlatList/SectionList/VirtualizedList
AlertIOS Alert
TimePickerAndroid   DateTimePicker  
DatePickeriOS  
PickerIOS Picker
VibrationIOS   Vibration

Another point to consider, is that the Facebook team responsible for React Native has been extracting a lot of components from the React Native core and putting them into separate repositories in react-native-community to make the core lighter and on the other hand, to facilitate the involvement of the open-source community. The following components were extracted from the RN core:

Deprecated component Alternative (React Native 0.60.4)
AsyncStorage @react-native-community/react-native-async-storage
ImageStore expo-file-system or react-native-fs
MaskedViewIOS @react-native-community/react-native-masked-view
NetInfo @react-native-community/react-native-netinfo
Slider @react-native-community/react-native-slider
ViewPagerAndroid @react-native-community/react-native-viewpager
ImageEditor @react-native-community/image-editor  
StatusBarIOS @react-native-community/status-bar  
PushNotificationIOS   @react-native-community/push-notification-ios  
WebView react-native-community/react-native-webview  
SegmentedControlIOS   react-native-segmented-control
Navigation react-navigation  
Geolocation @react-native-community/geolocation  
CheckBox @react-native-community/checkbox  
CameraRoll @react-native-community/cameraroll  

React Native upgrade (Android and iOS)

The React and React Native teams and numerous open-source contributors are constantly working on performance improvements and on adding new important features. The RN team is also working on extracting some of the components and moving them to react-native-community to reduce the RN core size and thus improving the performance.

For instance, React Native 0.59.1 introduced the 64-bit support for Android applications, that allowed to substantially improve performance. It was also one of the Google requirements for Android applications to be published in Google Play Market starting from August 1, 2019.

To get all the benefits of newer versions of React and React Native make sure to update the RN version your app is using on the regular basis. However, as I stated before, all optimizations, and especially React Native upgrade should be done with a great caution. Make sure, you do regression testing and check that all your app dependencies and features work with the RN version you are upgrading to. The best and safest approach, especially for large applications, is to do this upgrade through a bridge app, making sure every feature works.

For more information on React Native upgrade process, see this article. You can also refer to the official Facebook documentation here, though it is not as good and detailed as the above-mentioned article.

Dependency optimization (Android and iOS)

Analyze your dependencies and remove all unused packages and import statements. the ones that can be replaced with JavaScript built-in functionality. It will not only allow you to reduce the production bundle size but might also improve your app performance.

Don’t use spread operator for props (Android and iOS)

Avoid passing props that are not required by the component you are passing them to. I noticed that in many tutorials and articles on the Internet, the authors were using the spread operator  to pass props like in the example below:

<LogViewer {…props}/>

This approach can cause unnecessary rendering and thus negatively affect the app performance. To avoid that, pass only props that are really needed in the component, like in the example below:

<LogViewer

  logs={this.state.logs}

  populateLogs={this.populateLogs}

  setShowLogs={this.setShowLogs}

  showLogs={this.state.showLogs}

  logsReady={this.state.logsReady}

  setLogsReady={this.setLogsReady}

/>

Avoid using Array Index as Key (Android and iOS)

In React, Keys help identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.

Facebook React team doesn’t recommend using indexes for keys if the order of items may change, because it can negatively impact performance and may cause issues with component state. If you choose not to assign an explicit key to list items, then React will default to using indexes as keys.

Keys used within arrays should be unique among their siblings, but they don’t need to be globally unique. We can use the same keys when we produce two different arrays.

<FlatList
  refreshing={isRefreshing}
  style={
Styles.logsFlatList}
  data={logs}
  keyExtractor={(item, index) => index.toString()}
>

The rest of the code

</FlatList>

In the code fragment above, the index in keyExtractor was used as a Key. During the initial testing this approach was not causing any issues. However, after adding the list sorting feature and swipe-to-remove functionality this approach caused removal of multiple FlatList items when the user was swiping right to remove only a single FlatList item.

<FlatList
  refreshing={isRefreshing}
  style={
Styles.logsFlatList}
  data={logs}
  keyExtractor={(item) => item.log_record_id.toString()}
>

The rest of the code

</FlatList>

After replacing the index with the log_record_id in keyExtractor, which was unique for each log, the problem with the removal of multiple FlatList items was solved.

Avoid using anonymous functions in the component render function (Android and iOS)

Usage of anonymous functions in component’s render function is not recommended because the anonymous function will be re-created every time the component re-renders, and this will have negative impact on app performance.

In the example below, every time the component render method is called, the anonymous function in onChangeText is re-created. 

render () {

  return (

    <View style={Styles.tableHeaderContainer}>

      <View style={Styles.logsPerPageContainer}>

        <TextInput

          style={Styles.logsPerPageInput}

          keyboardType={‘numeric’}

          onChangeText={ (logsPerPageString: string) => {

                  let logsPerPage = parseInt(logsPerPageString);

                  if (isNaN(logsPerPage)) {

                           logsPerPage = 0;

                  }

                  const newIndexPager = new IndexPager(0, logsPerPage);

                  this.setState({ indexPager: newIndexPager });

         }}

         value={evaluateLogsPerPage(logsPerPage)}
        />
        <Text style={Styles.logsPerPageText}>Logs per Page</Text>
      </View>
    </View>
  );
}

To fix this, we need to extract the anonymous function and put it outside of the component render method.

render () {
  return (
    <View style={Styles.tableHeaderContainer}>
      <View style={Styles.logsPerPageContainer}>
        <TextInput
          style={Styles.logsPerPageInput}
          keyboardType={‘numeric’}
          onChangeText={this.onLogsPerPageChange}
          value={evaluateLogsPerPage(logsPerPage)}
        />
        <Text style={Styles.logsPerPageText}>Logs per Page</Text>
      </View>
    </View>
  );
} // End of Render function

// Extracted anonymous function

onLogsPerPageChange = (logsPerPageString: string) => {

  let logsPerPage = parseInt(logsPerPageString);

  if (isNaN(logsPerPage)) {

    logsPerPage = 0;

  }

  const newIndexPager = new IndexPager(0, logsPerPage);

  this.setState({ indexPager: newIndexPager });

};

Use functional components or Pure Components instead of Classes (Android and iOS)

You  can get some performance gains from using functional components, or pure components instead of class components. With the introduction of Hooks, and higher order components, such as React.memo, you can do the same things in functional components as in class components, getting a better performance.

You can find more information on functional components and React Hooks on the official website (https://reactjs.org/docs/react-api.html). The React component performance comparison can be found on https://www.telerik.com/blogs/react-component-performance-comparison.

Avoid unnecessary rendering (Android and iOS)

For React Classes we have the shouldComponentUpdate method, which is called right before rendering the component and that can be used to avoid unnecessary rendering and thus, avoiding waste of resources.

       shouldComponentUpdate(nextProps, nextState){

             if (nextProps.something !== nextState.something) {

                    return true; // Will cause the component to render

             }

             return false; // Will cause the component not to render

       }

In functional components we can use React.memo to avoid unnecessary rendering.

const component = React.memo(functionComponent(props){

// do something

});

It should be noted that React.memo will only shallowly compare the props, which in some cases can lead to bugs.

You can pass a compare method as a second argument to React.memo.

function component(props){

  // do something

}

// Comparison method

function areEqual(prevProps, nextProps){

  /*

  return true if passing nextProps to render would return

  the same result as passing prevProps to render,

  otherwise return false

  */

}

exportdefault React.memo(component, areEqual);

You can also use the React.PureComponent, that implements shouldComponentUpdate() with shallow props and state comparison, to avoid unnecessary rendering.

Conclusion

In this article, I tried to summarize the most common techniques for React and React Native performance optimization. This article is not final, and I will be updating it and adding more content on the topic along the way. Some of the techniques from this article deserve a separate article, some of which I will write later. I am also planning to run benchmarks to get quantitative data on performance gains for every technique and update the article later. Finally, I want to warn you that in some cases application of some of the optimizations from this article can have the opposite effect on your app performance, so you have to be careful before applying them and test your app thoroughly.

You can find official documentation on React Native performance optimization here https://facebook.github.io/react-native/docs/performance.

Leave a Comment