Loading, No Longer: Cutting Down Your App’s Startup Time

Serhii Tanskyi
Preply Engineering Blog
5 min readAug 31, 2023
Skyrocketing app startup time

In the bustling world of mobile applications, the initial few seconds can make or break a user’s experience. As developers, we strive for that electrifying first impression, that seamless startup, that instant ‘wow’ factor. But in the age of fleeting attention spans, ensuring our apps load swiftly and smoothly is not just a nicety — it’s an absolute necessity. This is particularly relevant for apps built using React Native. With its promise to bridge the gap between iOS and Android platforms, React Native offers a unique challenge: how can we optimise startup performance without compromising on the cross-platform benefits?

React Native has undoubtedly revolutionized how we approach mobile development. But as with any powerful tool, its capabilities come with complexities. Delve into this article to discover our initial steps, insights to boost the startup performance of your React Native app. Whether you’re a seasoned developer or just beginning your journey with this framework, there’s something valuable in these pages for everyone.

Essential Benefits of App Startup Optimisation:

  1. Enhanced User Experience: Swift-loading apps establish an immediate positive interaction, ensuring users feel the app is reliable and responsive.
  2. Higher Retention Rates: Faster startups directly correlate with user loyalty, making them more likely to return.
  3. Competitive Advantage: In saturated markets, a quick startup can make your app stand out against competitors.
  4. Cost-Efficiency: Optimised startup processes can lead to reduced server and operational costs.
  5. Stronger Brand Image: Immediate and seamless startups enhance overall brand perception and trust.

Startup optimisation aims at reducing the time taken by the app from the moment it is launched until it becomes responsive to user interactions.
Measuring the startup time in a React Native app can be approached in several ways, primarily depending on the platform being used (iOS or Android). This process typically involves calculating the duration from when the app is launched until it is ready for user interaction. Here’s how we measure startup time in both environments:

iOS

In AppDelegate.mm inside didFinishLaunchingWithOptions get process start time and pass it to launch Args:

CFTimeInterval startTimestamp = getProcessStartTime();
argsdict[@"native_startup_timestamp"] = [NSString stringWithFormat:@"%f", startTimestamp * 1000.0];
CFTimeInterval absoluteTimeToRelativeTime = CACurrentMediaTime() - [NSDate date].timeIntervalSince1970;
CFTimeInterval sPreMainStartTimeRelative = startTimestamp + absoluteTimeToRelativeTime;
argsdict[@"native_startup_time_ms"] = [NSString stringWithFormat:@"%f", (CACurrentMediaTime() - sPreMainStartTimeRelative) * 1000.0];

Android

Creating StartTimeProvider.java:

package com.preply;

public class StartTimeProvider {

private static long startTime = 0;

public static long getStartTime() {
return startTime;
}

public static void init() {
long currentTimeMillis = System.currentTimeMillis();
startTime = currentTimeMillis;
}
}

Put startup time to launch arguments:

long startTime = StartTimeProvider.getStartTime();
launchArgs.putString("native_startup_timestamp", String.valueOf(startTime));
launchArgs.putString("native_startup_time_ms", String.valueOf(System.currentTimeMillis() - startTime));

As a next step you can handle this timestamps inside javascript(for both platforms we have the same output):

const { native_startup_timestamp, native_startup_time_ms } =
InitialArguments.getInitialArguments() ?? {};

if (native_startup_timestamp) {
const startupTime = Math.round(Date.now() - parseFloat(native_startup_timestamp));

// you can log this data to any third party service

This data we are logging to our internal Data Warehouse service and then creating a realtime monitor in DataDog.

How we can improve?

As you can see we have huge startup time on Android, let’s try to find the root cause of this issue. React Native applications often use the Hermes engine to improve startup times and reduce memory usage, especially on Android devices. While Hermes has brought about significant performance improvements, one known limitation it has is its lack of built-in support for the ECMAScript Internationalization API (also known as Intl). The solution is to use the intl polyfill. If you need the full Intl API support, you can manually add the intl package, and set the polyfill for Hermes. Preply App supports localization to 15 languages, but we were adding them one by one and haven’t noticed how much polyfills we have:

import "@formatjs/intl-getcanonicallocales/polyfill";
import "@formatjs/intl-locale/polyfill";
import "intl";
import "@formatjs/intl-numberformat/polyfill";
import "@formatjs/intl-numberformat/locale-data/en";
import "@formatjs/intl-numberformat/locale-data/ru";
import "@formatjs/intl-numberformat/locale-data/fr";
import "@formatjs/intl-numberformat/locale-data/de";
import "@formatjs/intl-numberformat/locale-data/es";
import "@formatjs/intl-numberformat/locale-data/pt";
import "@formatjs/intl-numberformat/locale-data/it";
import "@formatjs/intl-numberformat/locale-data/pl";
import "@formatjs/intl-numberformat/locale-data/tr";
import "@formatjs/intl-numberformat/locale-data/uk";
import "@formatjs/intl-numberformat/locale-data/ar";
import "@formatjs/intl-numberformat/locale-data/ko";
import "@formatjs/intl-numberformat/locale-data/ja";
import "@formatjs/intl-numberformat/locale-data/id";
import "@formatjs/intl-numberformat/locale-data/zh";
import "@formatjs/intl-datetimeformat/polyfill";
import "@formatjs/intl-datetimeformat/locale-data/en";
import "@formatjs/intl-datetimeformat/locale-data/ru";
import "@formatjs/intl-datetimeformat/locale-data/fr";
import "@formatjs/intl-datetimeformat/locale-data/de";
import "@formatjs/intl-datetimeformat/locale-data/es";
import "@formatjs/intl-datetimeformat/locale-data/pt";
import "@formatjs/intl-datetimeformat/locale-data/it";
import "@formatjs/intl-datetimeformat/locale-data/pl";
import "@formatjs/intl-datetimeformat/locale-data/tr";
import "@formatjs/intl-datetimeformat/locale-data/uk";
import "@formatjs/intl-datetimeformat/locale-data/ar";
import "@formatjs/intl-datetimeformat/locale-data/ko";
import "@formatjs/intl-datetimeformat/locale-data/ja";
import "@formatjs/intl-datetimeformat/locale-data/id";
import "@formatjs/intl-datetimeformat/locale-data/zh";
import "@formatjs/intl-pluralrules/polyfill";
import "@formatjs/intl-pluralrules/locale-data/en";
import "@formatjs/intl-pluralrules/locale-data/ru";
import "@formatjs/intl-pluralrules/locale-data/fr";
import "@formatjs/intl-pluralrules/locale-data/de";
import "@formatjs/intl-pluralrules/locale-data/es";
import "@formatjs/intl-pluralrules/locale-data/pt";
import "@formatjs/intl-pluralrules/locale-data/it";
import "@formatjs/intl-pluralrules/locale-data/pl";
import "@formatjs/intl-pluralrules/locale-data/tr";
import "@formatjs/intl-pluralrules/locale-data/uk";
import "@formatjs/intl-pluralrules/locale-data/ar";
import "@formatjs/intl-pluralrules/locale-data/ko";
import "@formatjs/intl-pluralrules/locale-data/ja";
import "@formatjs/intl-pluralrules/locale-data/id";
import "@formatjs/intl-pluralrules/locale-data/zh";
import "moment/locale/ru";
import "moment/locale/fr";
import "moment/locale/de";
import "moment/locale/es";
import "moment/locale/pt";
import "moment/locale/it";
import "moment/locale/pl";
import "moment/locale/tr";
import "moment/locale/uk";
import "moment/locale/ar";
import "moment/locale/ko";
import "moment/locale/ja";
import "moment/locale/id";
import "moment/locale/zh-cn";

Loading all polyfills, regardless of whether your application needs them or not, can lead to unnecessary code being added to your bundle. Only include polyfills for the features your application uses.

As a first step we reviewed changelog of Hermes engine and removed redundant polyfills:

if (global.HermesInternal) {
require("@formatjs/intl-locale/polyfill").default;
require("@formatjs/intl-pluralrules/polyfill").default;
require("@formatjs/intl-datetimeformat/polyfill").default;
}

Then we load polyfills for specific language that App is using:

React.useEffect(() => {
if (languageCode) {
momentLocales[languageCode]();
if (global.HermesInternal) {
languagePolyfills[languageCode]();
}
moment.locale(matchMomentLocale[languageCode]);
}
}, [languageCode]);

Another way of optimising startup time is using lazy loading of heavy modules. Lazy loading is a design pattern where resources are loaded only when they are needed. In the context of React Native, this could be loading certain JavaScript modules only when they are required by the user. This can significantly speed up the startup time as not all resources are loaded upfront.

Here is example how to load heavy module in RN app:

const HeavyComponent = React.lazy(() => import("./HeavyComponent"));

export const HeavyComponentLoadable = () => {
const isHeavyComponentAvailable = useIsHeavyComponentAvailable();

if (!isHeavyComponentAvailable) {
return null;
}

return (
<React.Suspense fallback={null}>
<HeavyComponent />
</React.Suspense>
);
};

What we achieved with all these steps?

After applying these practices, we improved the startup time for Android by an average of ~16–20%.

Conclusion

In the ever-evolving landscape of mobile applications, performance stands as both a challenge and a testament to an app’s quality. We’re proud to announce that our dedicated efforts have culminated in a substantial ~20% boost in our app’s startup time, a significant milestone in ensuring our users have the seamless experience they deserve.

Yet, this achievement marks just the beginning of our journey. Performance optimisation is not a one-time task, but a continuous commitment to excellence. We recognize its paramount importance, and as we move forward, we’re poised to invest even more in enhancing every facet of our app’s performance. For our readers and users who are keen on following our progress, stay tuned. We pledge to share our journey, discoveries, and insights through more articles, showcasing our unwavering dedication to pushing the boundaries of app optimisation.

In essence, our pursuit of perfection never ends, and we’re excited to have you join us on this exhilarating expedition.

--

--