How do you know the performance of your React Native app is good? Or, say youâve implemented a fix and it might âfeelâ more fluid, but can you be scientifically sure those changes did improve performance?
Flashlight đŠ was built for this. Itâs a tool to measure performance of any Android app (though iOS support is on the way) aggregating different performance metrics. Iâll show you how to use it on a simple example app: a Netflix React Native clone.
Letâs take a simple app as an example: our main screen displays a vertical list composed of horizontal lists of movie pictures (all coming from TMDB)
Since FlashList, Shopify's component to display performant lists, came around, it'd be interesting to verify: is FlashList really better than FlatList for our app?
But this article should really illustrate how to use Flashlight to assert whether your code changes improved performance or not. It could be
FlatList
to FlashList
Essentially our vertical list looks like this:
const App = () => {
const playlists = require("./api/data/rows.json");
return (
<FlatList
data={playlists}
keyExtractor={(playlist: Playlist) => playlist.id}
renderItem={({ item: playlist }: { item: Playlist }) => (
<MovieRow playlist={playlist} />
)}
contentContainerStyle={listStyles.container}
/>
);
};
and our rows look like:
const MovieRow = memo(({ playlist }: { playlist: Playlist }) => {
const movies = playlistData[playlist.id]();
return (
<>
<Text numberOfLines={1} style={rowStyles.title}>
{playlist.title}
</Text>
<View style={rowStyles.container}>
<FlatList
contentContainerStyle={rowStyles.listContainer}
keyExtractor={(movie: Movie) => movie.id.toString()}
ItemSeparatorComponent={MarginBetweenItems}
horizontal
data={movies}
renderItem={({ item }: { item: Movie }) => (
<MoviePortrait movie={item} />
)}
/>
</View>
</>
);
});
You can check out the full code here
Switching to FlashList
is simple enough:
estimatedItemSize
to the lists, which FlashList
conveniently suggests for youFlatList
(you can see why in this FlatList optimization exercise) but no need with FlashList
The full diff is available here, quite small right?
Unlike FlatList
, FlashList
uses recycling.
Basically items do not get unmounted-remounted anymore, instead items disappearing from the screen are reused for new items appearing by changing their prop.
In the example above, when we scroll down:
You can get unintended side effects though. I shared a fun one I faced at the end of the article. Be sure to check the docs on how to make the most of it.
Side note, another list component is also coming: WishList by Margelo. When it comes around, Iâll update this article or post a new one to compare with FlatList and FlashList.
To efficiently compare performance, we should try the app on a low end Android device with __DEV__
mode disabled (see here to do it with Expo)
However, on the naked eye, there isn't necessarily a massive difference. This is where Flashlight comes in.
Flashlight aggregates performance metrics (FPS, CPU usage, RAM usage) and gives you a performance score, so that you can answer the question: is the performance of my app good?
Let's see how to use it with three different ways, each with their benefits:
The easiest way to get started with measuring performance is to install flashlight
:
macOS/Linux
curl https://get.flashlight.dev | bash
Windows
iwr https://get.flashlight.dev/windows -useb | iex
Then:
flashlight measure
and it will measure the performance of what you do in the app:The score depends on what you do during the measures and the duration of measures. For instance, If you do nothing, your score should be close to 100! (unless you have major performance issues).
In our case, ouch, pretty bad score. Seems like the main reason is a "high CPU usage per thread", what does this mean? đ€
Smartphones have multiple CPU coresâoften between 4 and 8âwhich enable them to run multiple threads in parallel. (For a deeper understanding, you can check out this article).
In Android apps, the UI thread is dedicated to rendering the UI smoothly, aiming for at least 60 Frames Per Second (FPS). It also handles user interactions like button presses. If the CPU usage for this thread nears 100%, it's a sign that it's overwhelmed, leading to a laggy user experience, such as choppy scrolling.
React Native apps have a similar structure but add a JS thread to the mix. This thread handles JavaScript logic and forwards rendering updates to the UI thread. High CPU usage on the JS threadâaround 100%âwill result in poor responsiveness. For example, onPress events may experience delays or fail to trigger.
Well obviously the score to start with! It's here to aggregate the different metrics and give you an easy-to-follow performance indicators.
However, React Native developers should be also interested in:
mqt_js
thread, which is the name of the JS thread on AndroidYour goal should be to ensure that the JS thread doesn't appear in the Processes with high CPU usage section.
In fact, your goal should be that nothing appears there, in addition to having your average FPS close to 60.
So in our case, our score is indeed pretty bad, because the JS thread is saturating for a while, meaning our app will be lacking responsiveness when scrolling.
flashlight
score heavily depends on the duration of measures. So when comparing measures, itâs important to measure for the same amount of time.
So to compare performance, we need to follow these steps:
Letâs do it once with the code using Flatlist
, then we can do it again after switching to Flashlist
and reloading (or rebuilding the app)
Pretty substantial gains right? đ„ł
Itâs worth noting thereâs no correlation between FlashList and Flashlight, except a lot of letters đ and the fact that using FlashList should improve your Flashlight score đȘ
With flashlight measure
, we were able to see easily that switching to FlashList has massive performance benefits in our case. But this approach could be optimized:
flashlight
score will vary if you repeat the same measures.To make the process less annoying and more deterministic, letâs start by automating our scrolling, we can easily script a scroll gesture on Android using adb shell input swipe
:
adb shell input swipe <x1> <x2> <y1> <y2> <duration>
will swipe from point (x1, x2) on the screen to point (y1, y2). duration
controls the duration of the swipe movement, so the lower, the faster the scrolling will be
On my phone, adb shell input swipe 500 700 500 300 50
works well for a scroll down. Our steps become:
adb shell input swipe 500 700 500 300 50
That's already a bit better! But that's too many clicks for me đ I find it still annoying especially when I want to quickly measure different performance fixes (possibly, Iâm lazy).
Luckily, we can automate all those steps with flashlight test
Given a test script to do things in our app (like our adb shell
command), we can use flashlight test
by passing as a testCommand
parameter:
flashlight test --bundleId com.movielist \
--testCommand "adb shell input swipe 500 700 500 300 50" \
--duration 10000 \
--iterationCount 1 \
--skipRestart \
--record
Ok, thatâs a big list of parameters, letâs explain:
bundleId
is your app bundle id or app id. You can find it with flashlight tools android_get_bundle_id
duration
tells Flashlight to measure for 10s (VERY important to keep the same when comparing measures)iterationCount
: Iâve mentioned that performance measures are not deterministic. This is why by default, flashlight test
will run 10 iterations of the test command and average the result. This should help in having more deterministic measures.--iterationCount 1
for instance, especially if the impact is clear--skipRestart
: by default, Flashlight closes the app before each iteration. This is useful if your test command starts the app, but in our case it doesnât so we add this to keep the app open.--record
: records a video while measuring (this one is a bit experimental still)At the end of it, a json
of measures is created, you can run flashlight report <...>.json
to open the web report similar to what we had with flashlight measure
đ„ł.
If you have several measure files, you can open the comparison view by running flashlight report first.json second.json
In our case, letâs generate 2 json of measures:
FlatList
:flashlight test --bundleId com.movielist \
--testCommand "adb shell input swipe 500 700 500 300 50" \
--duration 10000 \
--iterationCount 1 \
--skipRestart \
--record \
--resultsFilePath flatlist.json --resultsTitle "FlatList"
FlashList
and generate a new json:flashlight test --bundleId com.movielist \
--testCommand "adb shell input swipe 500 700 500 300 50" \
--duration 10000 \
--iterationCount 1 \
--skipRestart \
--record \
--resultsFilePath flashlist.json --resultsTitle "FlashList"
flashlight report flatlist.json flashlist.json
and get the comparison view đ„ł
We can be a bit more thorough with our measures.
For instance:
We could probably get by with simple adb
commands, but using a Maestro end-to-end test would be easier.
At BAM, we love Maestro for e2e testing and wrote about it in our tech radar. Installing it is simple, and creating a test to quickly scroll to the bottom of our list is easy-peasy!
Basically, we just have to create a scroll.yml
file describing what we want to do in our app, for instance:
appId: com.movielist # Replace with your app id
---
- launchApp
- assertVisible: .*Family.*
- scroll
Running maestro test scroll.yml
will
If you try this out, you might notice the scrolling isnât very fast. We can use swipe
instead of scroll
and play with the parameters:
appId: com.movielist # Replace with your app id
---
- launchApp
- assertVisible: .*Family.*
- swipe:
start: 50%, 90% # From (50% of width, 90% of height)
end: 50%, 10% # To (50% of width, 10% of height)
duration: 200 # Swipe takes 200ms, decrease to scroll faster
We can now repeat the scrolling to ensure weâve scrolled to the bottom using repeat
:
appId: com.movielist
---
- launchApp
- assertVisible: .*Family.*
- repeat:
times: 5 # Repeat enough times to scroll to the bottom
commands:
- swipe:
start: 50%, 90% # From (50% of width, 90% of height)
end: 50%, 10% # To (50% of width, 10% of height)
duration: 200 # Swipe takes 200ms, decrease to scroll faster
maestro test scroll.yml
should now open the app, and quickly scroll 5 times, which should be enough for our app to scroll to the bottom.
Now we can reuse this to get performance measures by running flashlight test
with --testCommand "maestro test scroll.yml"
.
Since the test takes a bit less than 30s in my case, letâs set --duration 30000
this time
flashlight test --bundleId com.movielist \
--testCommand "maestro test scroll.yml" \
--duration 30000 \
--record
Of course, it should take longer this times, but after running flashlight report
on both reports, you should be more certain which case has the best performance đȘ
That takes a few minutes though, right? Also if you want to compare measures between different members in your team, you have to ensure theyâre on the same phone!
This is why flashlight
also has a cloud version (totally free!! at least, for now), always running on the same low end Android device (a Samsung Galaxy A10s by default)
To use it, create your API key here and set it as an environment variable:
export FLASHLIGHT_API_KEY="fl_xxxxx"
Then you can use your Maestro test file as easily as this:
flashlight cloud --test scroll.yml --duration 30000 --app apks/flashlist.apk
Run it with both your APKs (before fix and after fix), then open https://app.flashlight.dev/test-list, select the 2 last reports and hit compare!
In my case this is what I get:
You can also check out the full comparison report here
Easy to see who the winner is, right? đ„ł
Letâs summarize a bit:
__DEV__
mode)measure | Quickly check performance bottlenecks in your app | |
test | Automate performance measures for faster iterations or more thorough measures | |
cloud | Do it all in the cloud on a real low-end Android device đ |
You can also check out how Thomas used flashlight
to measure Hermes vs JSC app startup performance in this article
React Native developers should be particularly interested in:
mqt_js
thread, which is the name of the JS thread on AndroidYour goal should be to ensure that the JS thread doesn't appear in the Processes with high CPU usage section.
In fact, your goal should be that nothing appears there, in addition to having your average FPS close to 60.
We had so many things to talk about! đ
If you have any questions or just want to talk about performance, feel free to reach out on Twitter, Github or Flashlight's Slack đ€
Iâve mentioned recycling can have unintended side effects. I actually got one myself: when rows get recycled, they keep their scroll position and momentum đ
Remember how recycling works? If our items are scrollable lists, then when item 3 gets recycled into item 6, it will keep scroll position and momentum of when it was actually item 3 đ€Ż
In my case, this produced something like this: I scroll the first line to the right. But when I scroll down, it gets recycled and I see another line scrolling.
The solution is to reset the scroll offset every time a new row appear, with a hook similar to this.
However, we also need this PR to land in React Native to make it fully work đ€
If youâve read my previous article about measuring performance, you might be wondering how Flashlight compares with the Performance Monitor Flipper plugin.
flashlight measure
aims to replace https://github.com/bamlab/react-native-flipper-performance-monitor. It is superior in a lot of ways:
However: