How we improved our React Native cold start for Android
About two years ago here at Mattermost, we decided to start building a prototype for our mobile apps in React Native (RN for short).
We did it for a couple of reasons:
- We wanted to share code between the mobile apps and the webapp, hence the birth of mattermost-redux.
- We wanted to be able to share code between our iOS and Android mobile platforms.
We were so impressed with how easy it was to build our app for both platforms that we ultimately decided that RN was the way to go for mattermost-mobile apps. Thanks to RN, we can focus exclusively on ensuring feature parity between our mobile apps and our webapp.
Experimenting with RN has been an incredible journey with highs and lows. Let’s not forget that RN is still a very young project that has a long way to go and several bugs to fix.
We’ve encountered a few challenges along the way:
- We weren’t sure which navigation library to use or what to use for state management (e.g., Redux or something else?).
- We needed to make sure we could handle regressions.
- We needed to ensure that we could support all the features our product has and will have in the future on both iOS and Android.
As we built the apps and started to implement more and more features, we encountered a major issue: Android’s Cold Start, i.e., how long it takes for an Android app to open from scratch. It was so bad that the majority of our users encountered the problem and were aware it existed the very first time they opened the app.
For some reason, our iOS app cold start was taking between two and four seconds to fully open and be ready to use. But on the other hand, our Android app was taking between eight and 15 seconds to fully open and be usable.
For a mobile app, these kinds of delays are unacceptable. This is why we decided to specifically work on improving the cold start on Android.
When we started to investigate the problem, we knew we weren’t alone. Unfortunately, compared to other more common problems like “how to solve navigation,” there aren’t lots of resources out there for improving cold start for Android.
To help developers who might find themselves in our situation in the future, we decided to outline a few things that we’ve done to remedy the problem and share it with the community.
The BIG entry point
The first thing you want to do is initialize your app as fast as possible. This is kind of obvious, right? It was for us. But we did a few things wrong.
First, we initialized the app by registering all the screens that the app uses, event listeners, managed configurations, configured push notifications, analytics and localization files. Because we were using redux-persist, we had a gate until the persistent store was rehydrated to identify that a user was logged in. After all that, the app was finally loaded.
Holy guacamole! All that before actually getting rid of the splash screen and showing the app to the user. No wonder it was taking a lot of time!
It was only going to get worse as we added new features. Thus, we started making adjustments so at cold start the only things that loaded and executed were things that were actually needed.
Take a look at these metrics to see how our loading times improved over time. To start. here’s what it looked like when a user opened the app for the very first time.
The light entry point
Now that we identified part of the problem, we needed to figure out how to fix it.
By this point, we only had enough information to understand that we were doing too many things before showing the first screen. So we refactored our app to do the following:
- Store the user identifier in the KeyStore when the user logs in so they don’t need to wait for redux-persist to rehydrate before knowing whether a user was already logged in. This made users able to see a login screen or inner-app screen as quickly as possible.
- Set the event handlers.
- Get the push notification token.
These changes, coupled with a few other small things, ended up saving almost two seconds.
As you can see, keeping your entry point as light as possible improves the cold start. Try and do only the minimum amount of initialization and then use transitions and animations when the rest of the app loads.
For iOS, users can use the JSC that is bundled with the operating system and is updated with every iOS update.
The developers at react-community are updating JSC for Android at regular intervals with their jsc-android-buildscripts project. Thanks to these efforts, there’s a more current JSC that can be used with our RN app bundle.
And so we gave it a try. Without any further changes to the app, here’s what we got.
Use the RAM Bundle feature (previously Unbundle) plus inline requires
If you read the docs about RN performance on the RN website, you’ll see that they explain concepts like “what you need to know about frames” and common problems such as running in dev mode and using console.log statements.
They also talk about profiling and pinpointing where your app is performing poorly and where you can try and improve it. Since RN version 0.55, they’ve added support for inline requires and “Unbundle,” which is now called RAM bundles.
Basically with RAM bundles (Random Access Modules bundle format) you get a few bundle.js files instead of one large file. Think of this like some sort of webpack for RN.
This is very useful when you have an app with lots of screens that you won’t even be showing to users when the app loads. In our case, some of these screens include the Channel Info, Thread, About, Channel Members and Settings. We can improve the load time of the bundle by using RAM Bundle to load only what we need first and then load the rest with inline requires.
Why does this matter?
The RN documentation explains how to enable the RAM format. Basically, with a few adjustments to your build.gradle file, you’ll enable it and then change your code to use inline requires when you don’t need to have that code loaded at start time.
Tip: When you create the modulePaths file that is used to generate the main bundle file, spend time reading through it and identify the modules that you actually need. Every app is different, but for most of them, you’ll find some development-specific files that you won’t need when releasing your app, as well as others that have a production counterpart instead.
In our case, after applying the RAM format, here are our results:
Once again, we were able to save another full second of loading time.
When we started our adventure of building our apps in RN, we found some performance issues on initial load time. With a bit of research, we were able to reduce load time from more than eight seconds to about four seconds—a 50% improvement, which in our case is huge. All it took was a few different configurations and code changes.
Hopefully you found this blog post useful.
I’m sure there are other things that can be done to improve Android loading times that we haven’t found so far. If you know any other ways to improve this, please feel free to join our Mattermost community server and have a chat with us. Or, if you prefer, drop me a line at elias[at]mattermost.com.
(Editor’s note: This post was written by Elias Nahum, Lead Software Developer at Mattermost, Inc. If you have any feedback or questions about How we improved our React Native cold start for Android, please let us know.)