These last few days, I had a chance to focus on improving the performances of the React Native app we build in our company: being already on both markets (iOS/Android), the approach I took was mainly JS-focused, in order to be able to deploy the tweaks via Microsoft’s Codepush tool.
I have to admit, I was quite surprised by how effective some of these changes have been — and this is why I’m sharing them with you all, hoping to help you understand that improving performances can be simple; not something that only the hardcore veterans can manage to pull off, as I used to think prior to this past week.
As anyone working with performances will tell you, prior to start improving performances you need to equip yourself with…🥁 something to measure them with 🎉
Within the react-native
base project there is a benchmarking tool calledPerfMonitor
. From my understanding, this is basically the same standard React Perf tool — which you can read more about here. Basically it provides you with an analysis of how much time (in ms) your app is wasting in useless re-renders; it has one flaw though, which is that it can only run in debugger mode, which is not optimised.
In any case, it is still a quite valuable piece of code to use — and this is how I had it set up:
I placed these few lines inside the ComponentDidMount of one of our root components, called Logged — which is the one basically “owning” the main navigation of a logged user. With these lines uncommented, on “boot up”, this tool would start recording everything that happens and, after 20 seconds — which in my mind was a good “plateau” time to have everything properly loaded in the app — it would print out its analytics about rendering.
I can’t go into the details of our app architecture, but what I think you should know is that the mentioned Logged component contains the main navigation tab bar (composed of 4 tabs) and that the first main tab contains its own tab bar navigation, again with 4 tabs (one of which uses Airbnb’s Map component).(and yeah, we use react-navigation)
The actual starting point
This was the baseline I worked with: before applying any sort of optimisation, this is what the Perf tool provided as benchmark.
Being an app already in v2.x, with more than one year of development under its belt, no wonder the codebase was not polished in every corner — thankfully using Eslint we managed to find and update all files not adhering to its rules.
We obtained a good ~50ms per Component improvement in the top 10 (with only a couple of exceptions, like CellRenderer). Good, but nothing major — so I kept digging.
Since some of our components have huge lists of items to show (like most apps), we need to react to props’ changes in specific ways. One of the checks we did in our componentWillReceiveProps was made on the list’s dataset, and when we changed the array comparison from the standard === to lodash _.isEqual() method, this is what happened:
This has been one of the most effective optimisation: apparently the lodash method is way better for comparing arrays — or maybe I’m just a noob and it was know by everyone except me 😇
Using this article I read a while back as a baseline, we decided to refactor a lot of component in order to make them functional. I was mind blown by the result:
Another huge performances improvement, by simple code refactoring! I was so excited: we dropped from ~750ms wasted on the heaviest component to a little less than 300ms!But just as I was prepping to 🍾…
REALITY. STRUCK. BACK.
It won’t surprise anyone knowing that the app communicates constantly with a backend, which provides most data that needs to be shown throughout the whole app.
Turns out that, during all my tweaking and fixing and being excited about performance improvements… our backend was not fully working 👾
So, just when I was about to run the last benchmark, my boss pinged me over Slack to let me know that he fixed a bug on the backend, and that finally the pins in the Map were showing again.
Oh, no…
I run the benchmark.
My reaction #truefact
Having (again) the map full of pins created a whole the new level of wasted ms.
(small disclaimer: don’t consider this a rant towards those devs who created & maintain the Map component, ok!? Those huge numbers are surely caused by my inexperience)
So, basically, I was back to square 1… but I couldn’t stop now, I was so close to performance heaven!
Thanks to the tool, it was fairly easy to see that the tab containing the Map component was not optimized… at all. So I focused my brainpower into adding some small fixes to the tabMap component, plus moving some portions to functional:
We got some improvements, again. Not enough though… time for one last try 💪
By changing the logic in the render()
method for the tabMap, I managed to have the pins&markers loaded only when the component is actually “shown” — the code is something similar to:
This last improvement managed to get us “back to good performances”, considering that all these measurements are over a 20000 ms timespan:
I was finally satisfied with the improvements: once we codepushed them to staging, the difference in the release version of the ⚛️ app was perceivable.
this MAY have been my boss reaction
A lot more can surely be done: I’m no JS expert, and surely Tal Kol would be able to code the performances up a lot more (if you haven’t ever heard of him or read his articles on React Native performances, go NOW) — but, hey, still got some solid results 😊
I seriously hope that this article may help you out with your own React Native apps, and that there are no major mistakes in what I’ve written above.In that sense, I’m really looking forward your feedback and/or suggestions: feel free to leave a comment or tweet me and I’ll be happy to talk with y’all 🤓
And, as always,
Happy coding! 🤖