In a real app
Measurement comes before any optimization, and only on release builds. On Android, adb shell am start -W and the Displayed line in logcat give TTID; reportFullyDrawn (callable from a native module once the screen is ready) gives TTFD. On iOS, the App Launch template in Instruments splits pre-main (dynamic framework loading, static initializers) from post-main. On the JavaScript side, performance.mark and the Performance panel in React Native DevTools show the time between bundle execution and the first React commit. A real low-end Android device is the reference, not the simulator.
The usual causes, in the order I find them: module-level work (SDK initialization, storage reads, large image requires) executed before the first render; a bundle that loads every screen when inline require or lazy-loaded routes could defer them; a cascade of blocking network requests behind the splash; heavy Intl polyfills; and on iOS a dynamic use_frameworks! that multiplies the frameworks loaded at pre-main. Hermes and its bytecode solve the parsing part, not these five.
What I look at first in a project: what runs between index.js and the first screen, how many providers are nested at the root, and whether the splash is hidden on a precise event (data ready) rather than a timer.
Related terms
Hermes
Meta’s JavaScript engine for React Native, the default since 0.70, which precompiles the bundle to bytecode at build time.
React Native DevTools
React Native’s built-in debugger since 0.76, based on Chrome DevTools: console, breakpoints, network, profiling, React tree.
Prebuilt (precompiled React Native)
React Native core shipped as precompiled binaries (iOS XCFrameworks, Android AARs) downloaded at build time instead of rebuilt from source.
Metro
React Native’s JavaScript bundler: it resolves, transforms, and serializes your code into a bundle, and runs the dev server.
FlashList
Shopify’s list, a FlatList replacement: it recycles cells instead of unmounting them, and v2 no longer asks for a size estimate.