Debug your React Native app performance issues

React Native

Performance

React Native is a really easy way to quickly develop an app for both iOS and Android platforms. But as your app grows larger, you might start to experience latency on taps, freezes or slow animations and transitions.

A lot of resources explain how to potentially fix these issues, but very few explain how to find out what is wrong in your code. Even the official documentation on performance profiling  is really scarce: only 5 paragraphs and no image 😢.

This article will walk you through a method to pinpoint, analyse and fix performances issues inside your React Native app. You'll know how to use the current tools at hands and hopefully get a better knowledge of how React and React Native work.

 

First step: Reproduce the issue

You had a call from a frustrated user which told you that they have to wait 10 seconds before being able to click that "confirmation" button. That seems like a nasty problem, but you've never experienced it with your simulator.

The first step is to be sure you are debugging the right problem. Sometimes with performance, the bug can only be reproduced with a lot of data: try to find out in which state the user was: which data did they have on display, at which rate was it being updated, and so on...

Once you can reproduce the performance issue on a low-end device, you can focus on analysing and solving the issue, and spend less time making guesses.

 

Second step : Use the thread profilers

Your problem may come from different sources. This article covers problems that lie in your JavaScript code - which are most of the issues you are likely to encounter. So you need to make sure that the issue is indeed a JS issue before going any further.

Open the developer menu on your simulator (Cmd+D for iOS, Cmd+M or Ctrl+M on Android) and click on "Show Perf Monitor".

You will see a new window like this one on the top of your screen:

1

This window shows the frame rate of your UI (native) and JS threads. The closer they are to 60, the better. Execute the behaviour that triggers the performance issue, and you should see your JS FPS dropping. If the bug is only visible on low-end devices, you might not see a significant drop.

If the UI thread is the only one dropping, that means the issue lies in the native code, and that means it's going to be a tad more difficult to debug. But before digging into some Java and Swift, check out the common fixes for UI issues below.

 

Third step : Profile your javascript

First Tool : Chrome 

N.B.: What follows cannot be done with the React Native Debugger, so make sure it is not opened when you start.

The best way to go is to use the Chrome browser tools. To do so, show the development menu on your simulator and select "Debug". A new tab will open on Chrome.

Open the dev tools and go to the Performance tab:

2

IMPORTANT: Start the performance profiling (by pressing Cmd+E) then reload your app (Cmd+R) while the profiler is still running. Stop the profiling. You need to do this because there is currently a bug that prevents the debugger to pick up the RN threads unless this has been done first.

Prepare your app so you can immediately trigger the performance issue.

On this tab, click the round button or hit Cmd+E to record a snapshot of your performance.
Once the recording has started, trigger the laggy behaviour on your simulator. Stop the recording when the behaviour stops - if it lasts more than 10 seconds, it might be wise to stop after 10 seconds to avoid having a huge process time.
When the results come in, you have to look for 2 lines: Timings and debuggerWorker:

3

N.B.: The Timings line is quite new (v84), so you might not see it yet - it will be available if you install Chrome Canary however.
These lines show what is happening in you JS thread, so let's dive a bit deeper:

Timings

The timings line shows you which components are re-rendered.

4

Use this tab to find components that you did not expect to render.
You can also use the tabs below to get more insightful data, with the Bottom up tab.

You can use 2 sorts in this tab: Self time and Total time. Self time represents the amount of time spent rendering the actual components (and not its children). Total time represents the amount of time spent rendering the component and all of its children.

5

If you have components with a huge self time, it is likely that they are doing a lot of work that can be optimised.

If you have components with a huge total time, you are probably rendering it too many times or you are rendering too many of its children.

Debugger Worker

This line shows the tree of Javascript calls:

6

It is less understandable than the Timings line, but sometimes you will have to use it if you are doing something that does not render components (e.g. fetching data from an API and doing some heavy computation with it).

If you want to know more about the Chrome profiler, I recommend Ben Schwartz's article.

Second Tool : Flipper

Flipper is a new native debugger developed by Facebook. It also works with React Native and has a few but growing amount of extensions.

If you have React Native > 0.62, your app should work outside the box with Flipper.

To debug your performance with Flipper, select the "React Devtools" under the "React Native" section on the right, then the "Profiler" tab on the top right corner of the window.

You can trigger a profiling session by clicking on the round button.

The profiling is done differently from the one in Chrome. Rather than focusing on your JS thread, it solely focuses on React renders and "commits". The commit phase is the moment React actually applies changes to the DOM (if you're on the web) or its native equivalent (if you use React Native).

The React Devtools presents its results in a flamechart. It represents all the currently mounted components of your app:

On the top right corner of the screen you can navigate between commits and filter some if they didn't take a lot of time to render - I suggest to remove commits with a time below 10ms to keep the ones that rendered a lot of components.

For each React "commit", you can see which component did render and why, and how much time it took them to re-render.

When you click on a component, it will filter the results to and zoom in to only display its children components and display why they rendered:

You can find out about the following issues with Flipper:

  • A component is rendered multiple times when you expected it to render once

  • A component is very heavy to render

  • Some components that you didn't expect to render did actually render

This is a bit more useful if you want to know directly what component is causing the issue. However, Flipper is still a bit unstable at the moment and can make your app and laptop crash, so I recommend to use the Chrome solution for now.

If you wish to master the React Profiler, you can read the blog post from the React team.

 

Fourth Step : Fix the issue

There's already a lot of resources on the net on how to fix your React Native issues, so I will point you towards the usual suspects for each diagnosis.

Causes for components rendering too many times or unexpectedly

  • You are not memoizing your component with memo

  • You are passing a new function, array or object to children components on every render, thus triggering a re-render - to understand why this is an issue I recommend this blog post

  • The memo function is not working as expected: you are changing props that should not (you can use the second argument of memo, equalityFn

  • Your selectors return different results on every call - this is often the case if you call .map or .filter on an array - to fix this you can use reselect

Causes for heavy JS work blocking your thread

  • You are doing a lot of .find or .filter on your store in your data selectors - to fix this, you can use normalizr

  • You have not optimized the complexity of heavy algorithms - to fix this, I'm afraid you'll have to dig into algorithm optimization

  • You are doing synchronously something that could be done asynchronously

  • You did not call runAfterInteractions to trigger some heavy work on an action

Known common UI optimizations

Other possible fixes

  • Some pages that are not displayed can be re-rendered if you update your data store. In this case you can listen on the focus and disable updates on your pages.

  • You could technically run the heavy JS on the UI thread if needed, but I haven't seen it documented anywhere. If you find out something, let me know!

  • You can also write some part of your code in C++ and run it thanks to JSI! See this demo if you're interested.