Hi everyone, super excited to be talking to you at
react Summit. I'm Alex, I'm a tech lead at BAM, we develop
mobile apps in Kotlin, Flutter, and of course
react Native. And I just love the subject of
mobile apps'
performance, so let's dive in. What does it mean for a mobile app to have good
performance? Well, according to this
video from Google, which will be available in the slides, your app should be able to run at 60 FPS, 60 frames per second. This means that your app should be able to draw 60 images per second when you scroll down, for example, to give an impression of smoothness to the user. It's basically like a movie. It's animated pictures, basically. But the question is, what about
react Native apps? Well, this also applies to
react Native apps, but there is an added complexity, the JS thread. Because most of your logic will run on the JS, most of your business logic probably resides on the JS side of things, you need to make sure that it's not too busy. For example, here I have this app with a Click Me button. When I click it, the state updates. I've been clicked one, two, three times. But Kill.js is too expensive, so when I click it, the JS side of things is blocked. So even if I click Click Me a lot of times after clicking Kill.js, nothing happens. And it's not before Kill.js has finished that the JS actually becomes responsive again, and you can see it update four to 12 times like that. So it's very important to take a look at the JS side of things as well, because your app could be running natively at 60 FPS, but be completely unresponsive. This is why
react Native offers this view, the
react Native
performance monitor, displaying UI and JS FPS, and this is why we created this Flipper plugin to be able to display it in a graph. Also, as an added bonus, it gives you a nice score for you to be able to run
performance benchmarks. But chances are, this score could depend on a lot of factors, actually. So let me give you some tips, general tips, actually, on
performance measures. The first one is this. You should test on a lower-end device. I mean, if you only test on a high-end device, chances are that you will miss most of the issues that your users are actually could be having. An iPhone 13 can actually run
javascript or some calculation 10 times as fast as a Samsung Galaxy A21s. So, well, you know what to do. You should definitely test on a lower-end Android device. Second tip is this. You should make your measures as deterministic as possible.
performance measures are hardly deterministic, so you can make several iterations and average the result. You can also make sure to keep the same conditions as much as possible for every measure, for example, network, the
data you were loading, et cetera. And I mean, if you want to reproduce the same conditions, it's ideal to be able to automate the behavior you want to test. And for that, you don't necessarily need an end-to-end
testing framework. You can just use ADB on Android, for example. Here with ADB shell input swipe, you can actually trigger a scroll on your app. Third tip is this. Disable JS dev. If you want to have true
performance measures on the JS side, you should disable this, because you might encounter issues that you won't actually see in production. And the fourth tip is, well, if you find some issues when measuring, well, just use the best analysis tools. On the JS thread, that would be
react devtools or even running a JS Flame Graph with the Hermes profiler. And on the UI side, use native tools like Android Studio, Systrace Profiler, or Xcode Instrument on iOS. All right, let's dive in on a concrete example now. TF1 is a TV news channel, and we're building their news app. And we wanted to make sure that
performance was on par with our quality
standards. So we checked the home feed, and our goal was that when we scroll down, basically on a low-end device, we should have JS FPS always above zero to keep the JS side of things kind of responsive, and the UI FPS always to 60 to be able to be very smooth. So following the four essential tips that I talked about before, basically, we use a Samsung J3 from 2017. It's our favorite low-end device. It's pretty low-end. We set up measuring for 10 seconds. Arbitrary, but it's just important to keep the same time for every measure so you're able to compare. We reload with JS state mode disabled. We wait for the feed to be loaded, and we hit Start Measuring on the Flipper plugin. We run ADB shell input swipe, blah, blah, blah. And we wait until the end of the 10 seconds, and then we reproduce this five times just to be able to see if we get similar results and average it. And the result was this. The plugin was giving us a score of 40 out of 100. JS was dead at zero for about four seconds. UI thread was kind of okay. But basically, this means that the app was actually not responsive for about four seconds, which needless to say was completely unacceptable. So since the issue was mostly on the JS side of things, we ran two
react devtools. And it's available out of the box in Flipper, which is nice. So first thing you want to do is click on this button on the top right, and there you can check the box, record why each component rendered like profiling. This is a neat option to activate. We will see why pretty soon. Then just click the blue button here to start recording. Do some stuff. So in our case, that would be reproducing the exact same scrolling with ADB shell input swipe, blah, blah, blah. And then we hit the button Stop Recording, and it gives us a result kind of like this. All right. First thing you want to do is check this. This is the list of comets. These are the phases where
react actually applies any changes. And so usually when you look at
performance issues, you want to find the most expensive comets in terms of how much time it took. And in our case, this would be the 11th one, because it's the biggest bar here. So let's click on it. And this is what it gives us. Pretty. There is a lot of colors here, but basically this is the hierarchy of your
react component. In gray here, you will see components that are actually not rendering. But in our case, the rest, green and some other colors is rendering. So if we zoom in a little bit on this part, for example, you will see the hierarchy of components starting with flat list here in gray, so not rendering. So our flat list, you know, our home feed was implemented with flat list. So this is it. And it's an internal implementation in the
react Native code. Its child is called a virtualized list. So here it is. And we have context of consumer. Its child is virtualized list, contact provider, et cetera, all the way down to context provider, just the list of children, basically. And this guy has several children called cell renderers. This is basically what is wrapping the stuff you pass in your render items. So essentially, the cell renderer are our list items of our flat list. You will notice also that on every component, you have two metrics. You have this one on the left side. For example, here, 4.9 milliseconds on our virtualized list. It's the self time. It's the time the component took to render without any of its children. And the right one is the total time. It's the total time that the component took to render, including all of its children. So here, basically, we have a virtualized list taking three seconds to render. And we can see that basically all of the list items are actually also rendering. So our list is rendering. All of the items in our list are rendering. And this takes a total of three seconds, which is kind of crazy. I mean, remember when we talked about the JS being locked for about three or four seconds? This totally explains it. We have three seconds for that. So what should we do? Well, the first thing that you want to do is differentiate between initial rendering. And when you hover a component, since we activated this option earlier, you will see something like that. This is the first time the component rendered. Or are those components actually re-rendering? They have been initially mounted, but they're rendering a second time or another time. And in this case, you will see something like this. In our virtualized list, it's a state change, a last change, which we don't really know about because it's the internal implementation of FlatList. In our case, actually, all of the components here were re-rendering. Because remember, I said that we have loaded our feed previously. So it was rendering already some items in the feed. So when we scroll down, basically, it's re-rendering even the top items in the feed. And this is crazy, right? They don't need to be rendered. So what is going on? Well, the first iteration we took to fix was this. And everyone should know about this. Because see, so the FlatList is a virtualized list. And virtualization works kind of like this. You have a long list of elements, for example, I don't know, 10,000. But of course, for
performance reasons, you're not going to be able to display them all at the same time. So we use a virtualized list to display only what the user sees and some items above and some items below to ensure smooth scrolling. It's important to notice that in
react Native, this is the FlatList property called window size. And in
react Native, actually, it's going to render 10 screens worth of items above the current viewport and 10 screens below it. So it actually renders a lot of items. Good thing to keep in mind. But so how does this work? Well, basically, the FlatList is keeping a state in its internal implementation of the first element to render and the last element to render. So when we scroll down, well, those change. And so this triggers a re-render of the virtualized list, which is normal. So basically, this means that render item, what you pass in your render item, is called onScroll. And this is by
design, which means you should absolutely follow the
best practices defined in the
react Native
documentation about FlatList. And you should memoize all of your list items. And that's what we did. And we went from this to this. So the big difference that you should notice is this. Basically, we have a lot of gray here. The items that we memoized are now not re-rendering. And this is what we want. We still have some green stuff above it. This is basically the internal implementation of virtualized lists, state changes, et cetera. So we can't really change, we can't really prevent it from rendering. And if it's not taking, I mean, what is really expensive here is the children. And we still have actually some green stuff here. But yeah, we'll get to that actually in a minute. Because first, we already have some less green stuff. We have some more stuff. I mean, we have less stuff rendering. So let's just check our
performance score. Let's check our measures, just like before on the Flipper plugin. And this is what it gives us now. We went from 40 to 52. Chase is only dead for like three seconds. And the UI thread is still around 60. So OK, it's not fantastic. But already just with a simple memo, we made a very good improvement to our
performance. But let's go deeper. Second iteration, I titled it the joy of nested list. And you will understand why pretty soon. Because yeah, remember those green stuff at the bottom? Let's zoom in to see what this is. So we have our cell render at the top. So this is wrapping or what we pass in our render item. Then we have the gray stuff, because we memoized it. But we have here something that is still rendering in green. And it's not a virtualized list. This is because we have horizontal carousels implemented with
react Native Snap carousels, which itself uses a flat list. So essentially here you see our seven carousels that are displayed on our home feed re-rendering. But why are they re-rendering when we scroll down? Well, remember the state, the window state we talked about before for the parent list, it passes it as context to the nested child list. So we can see here that the virtualized list are re-rendering because of context change. And this means that when we scroll the parent list, nested lists, even horizontal, the render item will also be called. So we have to memoize everything. And in our case, that would be the slide items. And so that's what we did. And then we went from this to this. Might be hard to see, but now we have a lot more gray at the bottom. So oh, we have some stuff appearing on the right side. We could check what this is. OK, they're actually re-rendering for the first time, which means that this is probably new list items appearing when we scroll down. This is OK. Let's check first excessive re-rendering. So now we have this. Lots of gray at the bottom. Let's check our score. And we went from 52 to 54. Pretty much the same, basically. Not super good improvement. So we need to go deeper. All right. Let's check a bit more those carousels. If we zoom in on this guy here, we can see that basically it's a virtualized list re-rendering. OK, our carousel. We've worked 10 slides. Wait, 10 slides? No, actually, our carousels only have four slides. Oh, but that's right. We added the loop prop on the
react Native Snap carousel, which means that it will actually add three slides on each slide of your carousel at the beginning and at the end to achieve the infinite loop effect. And the brightest among you will have noticed that this checks. This equals 10. So we decided to just deactivate this prop for
performance reasons. And so now we went from this to this. We can see that, indeed, we only have four slides rendering. So let's check our score. And our score is much better. We went to 70. Yes, it's only there for one second. Great, great. Still OK. All right. We're getting there, but still not perfect. So let's go even deeper. Right. Let's click on this first carousel, this virtualized list. We can see all of the items. If we zoom in on the first one, this is what we see. We see our virtualized list here. Still taking 75 milliseconds to not do anything, basically. But we noticed this guy, animated component, being in orange. So
react devtools will print out in a different color, like orange here, items which have a very big self time. So they're very expensive to render compared to their children. And so this guy here is expensive to render. But where is it coming from? So the top here is from the flatlist code, from virtualized list to cell render. This gray stuff is our slide. It's memoized. So here, this is coming from
react Native Snap Carousel. And yeah, actually, the animated components is here to have some cool, nice transition. But not only is it quite expensive to render, it's not even memoized. And it should be. So we could patch
react Native Snap Carousel, or at this point, we kind of realized that nesting virtualized list is tricky. So the question is, can we avoid it? And the answer is yes. Because remember what I said about virtualization by default in
react Native displaying 21 screens worth of items? In our case, we only have four items. So virtualization here for carousels bring no benefit whatsoever. So we decided to recode it ourselves with a scroll view and paging enable prop, basically. And now we have only gray here when we scroll down. Nothing is re-rendering excessively. So let's check our score. Now we are at 90. And the JS is never at zero. And the UI thread is always OK. Woo! We've done it. So I encourage you to try it out. Consider your app's
performance. Take a low-end Android device. Find areas with the plugin with zero FPS on JS side or below 60. Analyze those issues and fix them. And check back your score improvement. That's it. Thank you guys for watching. Hope you enjoyed the talk. If you have any questions, feel free to ping me on Twitter or follow me. I tweet mostly about
react Native
performance. Thanks, guys. See you next time.