Performance optimization tips in React Native

React-Native is developed with performance in mind however there are still some areas and practices that can shed extra performance.

In this guide, we will discuss some performance issues and fixes.
Before we go into optimization tips; what exactly are we trying to optimize? In the context of react native, we are mostly concerned about the frame rate of our mobile app; this directly influences the responsiveness and experience of our users.

What is frame rate?

This is essentially how videos (and user interfaces) work, it is a series of still images; when viewed in order at a certain speed, give the appearance of motion. Each image is a frame. The higher the number of frames displayed every second the more life-like and buttery smooth our UI seems to be.

Image of the UI thread and JS thread performance profile

In react native we have the main thread (UI frame rate) and the Javascript thread (JS frame rate). The Javascript thread executes all our javascript code (eg. application logic, API calls, state management, touch events) and the main thread is responsible for drawing anything UI related on our screen.

The UI and JavaScript threads are individually fast but performance issues might occur during communication between both threads via a ‘bridge’. An example is setState, doing heavy computations could lead to a drop in frames on the javascript thread. During that time animations driven by the javascript engine will experience lag and users might not be able to interact with the app even if it’s for a fraction of a second. It may seem inconsequential, but the effect is felt immediately by a savvy user especially anything that takes longer 100ms.

Tips to improve performance:

Use transform to animate the size of an image: Each time an image is resized, the image re-cropped and then scaled from the original image, this can be very expensive especially for heavy images. So always use the transform: [{scale}] style property.

Use InteractionManager: InteractionManager allows us to schedule task execution on the javascript thread after any interactions or animations have completed.

Avoid creating functions (anonymous functions) in render(): Creating functions in render is bad practice which could cause some serious performance issues. Each time a component re-renders a different callback is created; this might not be an issue for simple components, but a big issue for PureComponents, React.memo or when the function is passed as a prop to a child component, which would result in unnecessary re-renders.

Avoid prop drilling across components more than a level deep: Passing props from ancestor components through intermediary components down to a child component will cause unnecessary re-render of the parent, intermediary and child components which can be very expensive. To share data between components that are far apart use React.Context or a global state management tool like Redux.

Use react-native-screens: All screens are essentially native View from each native platform (Android & iOS) which increases memory usage and makes the render tree deep in a heavy-stacked application. react-native-screens allows for native screen optimization by exposing native navigation components like UIViewController for iOS, and FragmentActivity for Android. react-native-screens is used alongside any navigation library in React Native, to set it up with react-navigation. Follow this guide to set it up with react-navigation

Use LayoutAnimation: LayoutAnimation leverages Core Animation and it is not affected by JS thread and main thread frame drops. Unfortunately if you need more control over animations, like having to interrupt an animation, you will need to use the Animated API.

Add useNativeDriver to Animated config: The Animated API calculates each key frame on demand on the JS thread, which could lead to drop in the JS thread frame for complex animations. To prevent lag useNativeDriver passes all the data about an animation to the UI thread when it starts, meaning if there’s a lock or drop in the JS thread frame, the animation wouldn’t be affected. The drawback of useNativeDriver is that it only supports style properties like transform and opacity. To animate all style properties directly on the UI thread you could use a powerful library react-native-reanimated.

Use PureComponent or React.memo(): React and React Native components are great but they have one limitation; they aren’t smart enough to know when to re-render themselves or not based on its props or state. When we pass in non-primitive types, like objects we create a new reference every time (as they are immutable), thats why in javascript {} === {} is false. This leads to unnecessary re-renders in some cases. To prevent this we can use PureComponents or React.memo() to make a shallow comparison of every object property of the old props and next props.

Note: Use PureComponent or React.memo() responsibly the shallow comparison can be an expensive computation, only opt for them when a component renders a lot of unnecessary times. You can check out this article which discusses further why we need PureComponents and React.memo()

Use Hermes(Android): Hermes is an optimized Javascript engine optimized for running React Native on Android (and soon iOS 😎). Enabling Hermes results in improved start-up time, decreased memory usage, and smaller app size. Configure Hermes in your react-native project — https://reactnative.dev/docs/hermes

Make use of FlatList or SectionList for lists and its specific optimization: FlatList and SectionList contain several APIs for rendering very large lists and comprises of APIs (props) for performance improvements. Lets take a look at some tips and props for better list performance.

  • Use removeClippedSubviews — If true, views that are outside of the viewport are detached from the native view hierarchy.
  • Use maxToRenderPerBatch — This controls the amount of items rendered per batch, which is the next chunk of items rendered on every scroll.
  • Use updateCellsBatchingPeriod — This defined the wait time between batch renders.
  • Use initialNumToRender — Specifies the initial number of items to render
  • Use shouldComponentUpdate — Rather than using PureComponents which does some expensive shallow props and state comparison, use shouldComponentUpdate to create a custom rule for your list items.
  • Use getItemLayout — If all your list item components have the same height or width, make use of getItemLayout frees the javascript thread from calculating the layout of every list items. You could also make use of the library react-native-text-size to measure the size of arbitrary texts in your list items.
  • Use keyExtractor or key — The key prop or keyExtractor is utilized by React for caching of list items and tracking items re-ordering. Don’t use indexes as keys — This article explains why; Why using an index as key in react/react-native is probably a bad idea.
  • Avoid anonymous function on renderItem — Don’t define your list items render function in the renderItem prop, so it wouldn’t keep creating itself each time render function is called.

Conclusion

  • Inbuilt Show Perf Monitor from your app showing basic stats on the JS thread and UI thread
  • Flipper for general debugging and performance monitoring
  • why-did-you-render — why-did-you-render monkey patches React to notify you about avoidable re-renders.
  • Firebase Performance Monitoring — Firebase Performance Monitoring is a service that helps you to gain insight into the performance characteristics. You use the Performance Monitoring SDK to collect performance data from your app, then review and analyze that data in the Firebase console. https://rnfirebase.io/perf/usage

Notable mentions

  • react-native-largelist — Create list with very large data source with less CPU/Memory resource, great alternative to FlatList/SectionList, though I haven’t tested it myself.

SDE | Systems Engineering Student with a passion for the Web and Mobile dev 🤖 | Learning by doing, and sharing by writing ✍️ | https://github.com/dabigjoe6