One of the key metrics to measure the performance of your app is the frame rate. You want your app to run at 60fps (frames per second) to give an impression of smoothness.
This video from Google explains it well:
- 60 is high enough for good perceived smoothness
- there are less benefits in higher numbers
- your frame rate should be constant and not drop
Is this also valid for a React Native app?
Yes! But you also have to take into account the JS thread.
If you're having performance issues in your app, one way to figure out if it's coming from the UI thread (so likely an issue on the native side) or the JS thread, you can simply use the Performance monitor, available out of the box in the development menu.
If you see the JS thread going down a lot, checkout this great article by Louis to debug JS performance issues.
If you see the UI thread going down a lot on Android, you're in the right place! I'll show you how I solved some issues in our app, using Systrace.
If you'd rather watch than read, you can also checkout the lightning talk I gave on the subject at React Summit 2021:
Measuring the UI thread performance in production
The good news is the play store reports it out of the box! You can check out the Excessive slow frames stats in the Vitals section.
In our case, the numbers mean we had 22% of sessions for which more than 50% frames took longer than 16ms to render. Essentially, this means 22% of sessions in our app are laggy. This is horrendous! 😱
Checking the top apps on the play store, we can actually see that most apps have this number below 4%, much better.
Which means we definitely have some issues in our app!
Systrace dive in
It's time to investigate what the problem is. And for a native Android performance issue, a nice tool to use is Systrace.
Systrace is short for "System trace". It's a python script located in the
platform-tools folder of your Android SDK (for me, with an Android Studio install on macOS, it's
It's simple to use:
- Connect an Android device or an emulator to your computer with your app installed (to test performance, it's usually best to take a low end device to aggravate any performance issues)
- Then run
python /path/to/systrace.py -o trace.html sched gfx view -a com.yourappid(replace
/path/toby the proper path for your computer and
com.yourappidby your app bundle id)
- Use your app
- Then press "enter" to stop the trace in the terminal
- You should now have a
- In Google Chrome, open
chrome://tracing/and click "Load" on the top left to load your trace file. You should see something something like this:
Beautiful right? A bit daunting, but very colorful ☀️.
Note that Google is actively working on Perfetto, a better version of Systrace. But at this time, the features we need to investigate frame drops are not yet implemented.
All right, let's dive in. The first thing you want to do is find your app on the left side panel based on your bundle id:
Let's zoom in on this line. In the colourful center panel, you can navigate with the wasd keys, try it out! After zooming in with the
w key you should see something like this:
On that line, there are a lot of "F"s, some are red, some are orange, some are green. You might guess what that means. "F" is of course for frame. Red "F" = bad ❌, green "F" = good ✅. Indeed a green "F" corresponds to a frame that was able to be calculated under 16ms, which means 60fps can be achieved. Orange and red "F"s are failure to do so.
So if you want analyze performance issues in your app, you want to take a look at sources of red "F"s.
In my case, I had only opened the app and waited for a few seconds on the home screen, and the trace file was already giving me a lot of red "F"s, as you can see above. There were 2 major parts of the trace file concerned, here's how I found where those issues were coming from, thanks to Systrace:
1st method: Investigate the threads
On the left panel, you should see threads under your app.
For a React Native app, you would typically see:
mqt_js: this is the JS thread, so that can be useful to check when the JS thread is busy
mqt_native_modules: this is the thread used by bridges
OkHttpthreads, this is the native library used by React Native to make API calls
Frescothreads, this is the native library used by React Native to display images
But what interests us here is the UI thread: this is the thread that should be able to calculate rendering in 16ms or less to achieve 60fps. The performance monitor for React Native actually reports its frame rate.
First, it's a good idea to click "View Options" on the top right, and check "Highlight VSync". This will basically highlight all the 16ms frames in which calculation must be done.
Now if you zoom in (with the W key) on a single frame where you have a red "F", you should something like this for the UI thread:
For the UI thread, you see a stack of calls. The top one (which calls the one below it) is
Choreographer#doFrame, this is the one that should take less than 16ms. We can see that here, this is definitely not the case:
We see here that it's calling
draw. This is very low-level. Essentially this is Android going through the hierarchy of components and checking what needs to be redrawn on the screen. We can see here that the
draw function is taking more than 16ms, so there must be something very expensive redrawn on every frame here.
You might be more lucky and have more information in your trace, but in my case, I had to look further to find the issue.
We can scroll down a bit and find the Render thread. The rendering thread is running on the GPU. It runs heavy calculation with high performance (since it does it on the GPU) and forwards the results to the UI thread. For instance, if you use a native animation (using native driver on React Native) such as a translation, the calculation would run on the Render Thread, that's why the performance would be much better.
Here we're luckier! We can see there's function taking a lot of time to complete related to a Lottie animation:
In fact, we had a Lottie animation running at the start that needed optimization. Lottie has a neat guide on how to do it if you're curious.
With our Lottie animation optimized, that was the first issue solved! Let's take a look at the second one.
2nd method: Check what keeps the CPU busy
At the end of our trace file, we had still a lot of red "F"s. But we were not as lucky as before to find the root cause investigating what goes on in the threads.
Now's a good time to bring up the Kernel panel.
This panel, usually at the top of the trace file, tells us what each of the device CPUs is doing at any given time (my device, a Wiko Fever, has 8 CPUs).
For instance, we can see below that CPU 4 is running the JS thread for a few ms. We can also see CPU 2 doing something Fresco related for a short time.
A very useful feature is this: you can draw a rectangle with your mouse over a section:
and you will see in the panel below, if you click on Wall duration what's taking the most calculation time in that time frame.
In our case, we see that the Render thread is indeed quite busy. But we knew that already with our previous investigation. We don't know what's causing it to be so busy though.
The second one, however is more interesting:
It's called Chrome_InProcRe and a quick search reveals that this is related to a WebView. So it would seem that there is a WebView taking most of rendering time, and thus being the cause of performance issues in our app.
In fact, on the home screen, we have ads running inside a WebView and they (severely) needed to be optimized!
A happy conclusion
And with both our issues solved, let's admire the beautiful new trace full of green "F"s now:
And our slow frame stats on the play store are much better! 🥳
Systrace is a powerful tool to have in your toolbox and I hope it will help you as well! Let me know how you get on with it 😉