A React Native supermarket app takes 4 s from tapping its icon to a usable home screen on mid-range Android; how would you find and cut the biggest costs?
answer
- release build, real mid-range device
- split native init, JS, first render, data
- Expo Atlas for bundle contents
- defer screens and import-time work
- do not block home on the network
basics
~20 sMeasure cold starts on a release build on a mid-range Android phone, split the time into phases, then cut the largest: eager imports, import-time side effects, bundle bloat and network waits before the home screen.
solid answer
~50 sFirst measure honestly: a **release build** on a mid-range Android phone, several cold starts, time from tap to a home screen that responds. Break the 4 s into phases - native start, bundle load, JavaScript **evaluation**, first render, data, splash hide - with a trace. Then work down the list: confirm the Android bundle is stored uncompressed (the default since 0.79) and that Hermes is in use; inspect bundle contents (with Expo Atlas, `EXPO_ATLAS=true npx expo export`) for a bundled product catalog, duplicate libraries or unused locales; make every screen except home lazy with `getComponent`; move SDK initialisation and index-building out of module top levels; and stop the home screen and splash from waiting on the offers API - render cached data or a skeleton instead. Legacy RAM bundles are not an option: they do not work with Hermes and the CLI removed `ram-bundle` in 0.75. Re-measure after each change.
code
bash · 6 lines# Expo: see what the production Android bundle actually contains
EXPO_ATLAS=true npx expo export --platform android
npx expo-atlas .expo/atlas.jsonl
# Measure cold start on a release build, on a mid-range Android phone
npx expo run:android --variant releasego deeper
Know that startup is measured on a release build on a real device, and that loading less code and not waiting on the network before the first screen are the main levers.
Explain the phases of a cold start and which React Native settings and code patterns affect each one, including lazy screens and the uncompressed Android bundle.
Show a measured, ordered investigation: baseline on a mid-range device, trace to find the largest slice, remove network waits, defer modules, trim bundled data, re-measure.
Discuss turning the result into a startup target the team keeps: a reference device, a measured budget, and checks that catch regressions in review or CI.
## Start by measuring the right thing "Startup" means **cold start to interactive**: from tapping the icon on a device where the app is not running, to a home screen that shows real content and responds to touch. Measure it where it hurts: - a **release build**, since development mode inflates JavaScript cost; - a **mid-range or low-end Android phone**, where the 4 s was reported; - **several cold starts**, force-stopping the app between runs, taking the median. Then split the time. A system trace or a JavaScript sampling profile shows roughly: | Phase | Typical culprits | |---|---| | Native start and React Native initialisation | heavy native SDKs initialised eagerly | | Loading the JavaScript bundle | a compressed Android bundle, a very large bundle | | Evaluating modules | eager screen imports, import-time side effects, bundled data | | First render | a home screen that renders too much at once | | Data and splash | waiting on the network before hiding the splash or showing content | Fix the biggest slice first, and re-measure after each change. ## Loading the bundle - **Uncompressed on Android.** Since React Native 0.79 the bundle ships uncompressed by default so it can be memory-mapped. Check that nobody set `enableBundleCompression = true` (or `android.enableBundleCompression` in `expo-build-properties`). - **Hermes.** Hermes is the default engine and release builds ship precompiled bytecode, so parsing is cheap. JavaScriptCore left core in 0.81; if an old project still uses a community JSC setup, that is a startup question in itself. - **Not RAM bundles.** Random access module bundles were an older way to load modules lazily. The docs state they are not supported with Hermes, and the `ram-bundle` CLI command was removed in 0.75. ## Evaluating less JavaScript - **Look inside the bundle.** In an Expo project, `EXPO_ATLAS=true npx expo export` records the production bundle and `npx expo-atlas .expo/atlas.jsonl` opens it. Typical finds in a supermarket app: the full product catalog imported as JSON, several date or currency libraries, all locale files, a maps SDK pulled in by the home screen. - **Lazy screens.** Keep home static; load checkout, store locator, recipes and settings with React Navigation's `getComponent` so their modules are evaluated on first visit. - **No import-time work.** Move analytics and payment SDK initialisation, search-index building and storage reads out of module top levels into functions called when needed. ## Rendering the first screen sooner - **Do not wait on the network.** If the splash stays up until the offers API answers, time-to-interactive equals network latency. Hide the splash once fonts and the local session are ready, and render the home screen from **cached data** or a skeleton. - **Keep the first render small.** Render the above-the-fold content first; carousels and recommendation rows can follow. - **Prefetch after interactive.** Warm caches for other screens once the home screen responds. ## Keeping native start honest The JavaScript side is usually where the React Native-specific wins are, but the native phase is part of the same 4 s. Native SDKs that initialise eagerly at process start, and native modules the first screen touches, add time before any JavaScript runs. In the trace, check how long the app takes to reach the point where the JavaScript bundle starts loading; if that phase is large, look at which native libraries initialise at launch and whether they can wait until after the first screen. ## A worked order of attack 1. Measure the baseline on the target device. 2. Remove network waits from the startup path - usually the largest and cheapest win. 3. Make non-home screens lazy and remove import-time side effects. 4. Shrink the bundle's startup-relevant parts: bundled data, duplicate libraries. 5. Confirm the Android bundle packaging. 6. Re-measure, and keep the numbers so a later regression is visible. ## Pitfalls - Optimising in a debug build or on a flagship phone. - Chasing bundle bytes while the home screen waits two seconds on an API. - Making screens lazy that the home screen still imports statically. - Moving an SDK initialisation later without checking what depended on it. - Declaring victory after one run; cold-start times vary, so compare medians.
- Why are RAM bundles not a fix for this app's startup?RAM bundles stored modules separately so only the needed ones were parsed. The React Native docs state they are not supported with Hermes, whose bytecode already loads on demand, and the `ram-bundle` command was removed from the CLI in 0.75. Lazy screens and removing import-time work are the modern levers.
- The bundle is smaller after your changes but startup barely moved. What does that suggest?The time was probably not in loading the bundle. Look at the trace again: evaluation of modules imported at startup, a heavy first render, or a network wait before the splash hides are likelier causes than raw bundle size.
- How do you keep the home screen useful without waiting for the offers API?Render it from data cached on the previous launch, or from a skeleton, and update when the request returns. Hide the splash once local essentials such as fonts and the session are ready, so network latency no longer sits on the startup path.
saying these in an interview costs you the question
- Startup should be measured in a debug build on a simulator
- Switching to RAM bundles will speed up a Hermes app
- Bundle size is always the main cause of slow startup
- Holding the splash until the API responds is the safe choice
- Making screens lazy helps even if home imports them statically