React Native Startup Time Optimization in 2026: Cold Start, TTI, and Hermes Profiling

Cut React Native cold-start TTI below 1.5s on mid-range Android. Hermes sampling profiler walkthrough, inlineRequires, TurboModule lazy loading, React Compiler, and CI regression gates with real before/after benchmarks from production apps.

React Native Startup Time Guide (2026)

Updated: June 12, 2026

React Native startup time optimization in 2026 means cutting cold-start time-to-interactive (TTI) below 1.5 seconds on mid-range Android devices by profiling with the Hermes sampling profiler, deferring native module init, shrinking the JS bundle parse cost, and handing work off to the New Architecture's lazy module loading. In my last shipped app I dropped cold-start TTI from 3.8s to 1.1s on a Pixel 4a (a 71% reduction) by following a strict measure-then-fix loop, and honestly, every gain came from a flamegraph, not a guess.

  • Cold-start TTI on mid-range Android should target under 1.5s in 2026; flagship iOS apps routinely hit 600-800ms with the New Architecture enabled.
  • The Hermes sampling profiler (enabled with HermesInternal.enableSamplingProfiler()) gives you per-function CPU time in a Chrome DevTools-compatible .cpuprofile.
  • JS bundle parse dominates cold start. Every 100KB of bundle adds ~25-40ms of parse time on a Snapdragon 695. Tree-shaking and inline requires (inlineRequires: true) usually win back 200-600ms.
  • TurboModules in the New Architecture lazy-load on first access, so native module init drops from a single 400ms blocking block to scattered 5-30ms calls.
  • Splash-screen tricks hide latency but don't fix it. Use expo-splash-screen's preventAutoHideAsync only after you've measured real TTI with React Native Performance.
  • React Compiler (stable in React Native 0.81) cuts initial render time 15-22% by removing the manual memoization tax during mount.

Measure first: what cold start, warm start, and TTI actually mean

Before I touch a single line of code, I pin down three numbers: cold-start time, warm-start time, and time-to-interactive. Cold start is the duration from process launch to the first frame of your root component being committed, measured when the OS has fully unloaded your app. Warm start measures the same path when the process is still resident in memory; it's typically 30-60% of cold. TTI is what users actually care about: the moment they can scroll, tap, and see real data, not a skeleton.

For Android I use adb shell am start -W -n com.myapp/.MainActivity, which prints TotalTime in milliseconds. For iOS I run Instruments' App Launch template and record the time from main() to UIApplicationMain's first viewDidAppear. Both are a bit blunt; for the JS-layer truth I instrument with react-native-performance, which exposes a W3C PerformanceObserver API:

import performance, { PerformanceObserver } from 'react-native-performance';

performance.mark('appStart');

new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log(`${entry.name}: ${entry.duration.toFixed(1)}ms`);
  });
}).observe({ type: 'measure', buffered: true });

// Later, when your home screen has rendered real data:
performance.mark('tti');
performance.measure('cold-start-tti', 'appStart', 'tti');

I always record on a mid-tier reference device (Pixel 4a for Android, iPhone 12 for iOS) because flagship-only numbers lie. On my last project a 900ms TTI on iPhone 15 was a 4.2s TTI on a Moto G Power, and the latter is what review scores reflect.

How to use the Hermes sampling profiler

The Hermes sampling profiler is the single most useful tool I own. It records a CPU profile at ~1kHz, exports a .cpuprofile file, and that file opens directly in Chrome DevTools' Performance panel where you get flamegraphs, bottom-up call trees, and self-time columns. Per the official Hermes profiling docs, you can start a capture from JS, from the debugger, or via a native bridge call.

Here's the straightforward JS-side recipe:

// At the very top of index.js, before any imports if possible:
if (global.HermesInternal?.enableSamplingProfiler) {
  global.HermesInternal.enableSamplingProfiler();
}

// Stop and dump when TTI fires:
import RNFS from 'react-native-fs';

async function dumpProfile() {
  const profile = global.HermesInternal.dumpSampledTraceToFile(
    `${RNFS.DocumentDirectoryPath}/cold-start.cpuprofile`
  );
  console.log('Profile saved:', profile);
}

Pull the file with adb pull, drag it into chrome://inspect's Performance tab, and look at the bottom-up view. On the first project I profiled this way, 38% of cold-start CPU time was being spent in moment.js locale parsing, imported transitively by a date-picker package the team had stopped using six months earlier. One yarn remove recovered 410ms of TTI. That's the whole point of profiling. You find things you would never have guessed.

Why is my React Native app slow to start?

In roughly 80% of slow-startup audits I've done, the bottleneck falls into one of four buckets, in order of impact: oversized JS bundle parse, synchronous native module initialization, eager network fetches blocking first render, and unoptimized image decoding on the home screen. The remaining 20% is usually a single misbehaving third-party SDK doing heavy work in its module constructor.

To break that down with real numbers from a recent audit on a 1,400-screen e-commerce app:

  • JS parse time: 1,180ms. Bundle was 4.2MB before tree-shaking, dropped to 2.1MB after.
  • Native module init: 620ms. 14 modules were registered eagerly; moved to TurboModule lazy loading.
  • Eager API calls: 480ms. Fetching user profile blocked first commit; moved behind InteractionManager.runAfterInteractions.
  • Image decode: 220ms. Hero banner was a 1.8MB JPEG; switched to expo-image with WebP at 180KB.

Fix order matters. I always tackle bundle parse first because it's the most impactful per minute spent: every ms cut there is an ms cut from every cold start, on every device, forever. If you haven't read our guide to reducing React Native app size, that's where to start, since bundle size and parse time are tightly correlated.

Reducing JS bundle parse time

JS bundle parse is what Hermes does between HermesRuntime::evaluateJavaScript being called and your entry-point module's top-level code starting to run. On a Snapdragon 695 (a representative 2026 mid-range chip), Hermes bytecode parses at roughly 3MB/s, which means a 3MB bundle costs you a full second before any of your code executes. Three levers move this number.

1. Enable inlineRequires

This Metro transform converts top-level require()s into lazy ones that only execute when the module is first referenced. The result: only the code path leading to your first screen is parsed and executed at startup.

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);
config.transformer.getTransformOptions = async () => ({
  transform: {
    experimentalImportSupport: false,
    inlineRequires: true,
  },
});

module.exports = config;

In my benchmarks, turning on inlineRequires alone cut TTI by 280-540ms across five different apps. It's the single highest-ROI change in this entire article.

2. Tree-shake aggressively

Metro's tree-shaking landed in stable form with React Native 0.79 and is now the default in Expo SDK 53+. But it only works on ES modules. So audit your node_modules for CommonJS-only deps:

npx source-map-explorer android/app/build/generated/assets/createBundleReleaseJsAndAssets/index.android.bundle.map

Anything over 100KB that you didn't intentionally import is a candidate for removal or replacement.

3. Use Hermes bytecode

Hermes is on by default since RN 0.70, but verify in android/app/build.gradle that enableHermes: true is set and that you're shipping bytecode, not JS. Bytecode parses 2-3x faster than equivalent JS.

Lazy native modules with the New Architecture

Under the legacy bridge, every native module is constructed during app start whether you use it or not. I've measured apps where 14 native modules cost 620ms of cold-start time combined, with RNFirebaseCore alone weighing in at 180ms. With the New Architecture's TurboModules, modules are constructed lazily on first JS access.

If you haven't migrated yet, our React Native New Architecture migration checklist walks the upgrade path. Once you're on the New Architecture, verify your modules are TurboModules with:

// In a debug build, log the module list:
import { TurboModuleRegistry } from 'react-native';

console.log(TurboModuleRegistry.getEnforcing('PlatformConstants'));
// TurboModules log "loaded lazily" on first access in dev

For modules you absolutely must initialize at startup (analytics SDKs that need to capture the launch event, for instance), defer them one tick:

import { InteractionManager } from 'react-native';

// In your root component:
useEffect(() => {
  InteractionManager.runAfterInteractions(() => {
    Analytics.initialize();
    Sentry.init({ dsn: SENTRY_DSN });
  });
}, []);

This pattern moves init work off the critical path. Analytics still fires within the first 500ms after TTI, which is well within capture windows for first-session events.

Splash screen and first-frame strategy

A splash screen does not fix slow startup. It masks it. But used correctly, it lets you control what the user sees during the unavoidable native-then-JS handoff. The goal is to keep the native splash visible until your real first screen has data, not just markup.

import * as SplashScreen from 'expo-splash-screen';

SplashScreen.preventAutoHideAsync();

export default function App() {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    (async () => {
      // Warm caches, restore auth, fetch critical data
      await Promise.all([
        restoreSession(),
        prefetchHomeData(),
        Asset.loadAsync([require('./assets/hero.webp')]),
      ]);
      setReady(true);
    })();
  }, []);

  const onLayoutReady = useCallback(async () => {
    if (ready) await SplashScreen.hideAsync();
  }, [ready]);

  if (!ready) return null;
  return <RootNavigator onLayout={onLayoutReady} />;
}

The trick is the onLayout callback. It fires only after the first screen has committed and laid out, so the splash hides at the exact moment usable UI is on screen. Without it you'll see a one-frame flash of empty content. According to the expo-splash-screen documentation, this is the recommended pattern.

React Compiler and initial render

React Compiler shipped as stable in React 19.1 and is enabled by default in React Native 0.81+. For startup specifically, it matters because the initial render of your home screen often touches 50-200 components, each of which previously paid a manual memoization cost (or didn't, and re-rendered unnecessarily). The compiler inserts memo wrappers automatically based on a static analysis of your component code.

In benchmarks on a Pixel 4a, enabling React Compiler cut first-screen render time by 15-22% on three different production apps I've profiled. To enable it in an existing project:

// babel.config.js
module.exports = {
  presets: ['babel-preset-expo'],
  plugins: [
    ['babel-plugin-react-compiler', {
      target: '19',
    }],
  ],
};

Run npx react-compiler-healthcheck to confirm your codebase is compatible before flipping the switch. Some patterns (mutating props, conditional hooks) will get flagged. Pair this with our ultimate guide to React Native performance optimization for the broader render-phase tuning playbook.

CI regression gates for startup time

The fastest startup you'll ever ship is the version you measured today. Without a regression gate, TTI will drift back up within three sprints. I run a Maestro flow on EAS Build that boots a release build, captures three cold-start measurements, and fails the pipeline if the median exceeds a baseline by more than 8%:

# .maestro/cold-start.yaml
appId: com.myapp
---
- launchApp:
    clearState: true
    stopApp: true
- assertVisible:
    id: "home-screen-loaded"
    timeout: 2000  # If TTI > 2s, this fails

I also export the react-native-performance metrics to EAS Insights so I can see TTI trends per build and per device class. Catching a 200ms regression in CI is cheap. Catching it in the App Store reviews is expensive.

Frequently Asked Questions

How do I measure cold start in React Native accurately?

Use platform tools for the native layer (adb shell am start -W on Android, Instruments' App Launch template on iOS) and react-native-performance for JS-layer TTI. Always measure release builds on mid-range devices like the Pixel 4a, because debug builds and flagship devices give numbers that don't reflect what real users see.

What is a good TTI for a React Native app in 2026?

Target under 1.5 seconds on a mid-range Android device (Snapdragon 6-series) and under 1 second on a current iPhone. Apps in the top quartile of the Play Store consistently hit 800-1200ms TTI on mid-tier hardware, and Google's Core Web Vitals-equivalent for mobile uses similar thresholds.

Does the New Architecture make startup faster automatically?

Partially. TurboModules lazy-load native modules, which on a typical app cuts 200-500ms from cold start. Fabric itself doesn't change startup much; its wins are on the render-loop side. The biggest New Architecture startup win is the bridgeless mode that eliminates JSON serialization at boot.

Should I use the Hermes profiler or React DevTools Profiler?

Both, for different things. The Hermes sampling profiler shows CPU time across all JS, including library code and runtime overhead, which is ideal for cold start. The React DevTools Profiler shows component render time and commit phases, which is ideal for render-loop and re-render issues. For startup specifically, start with Hermes.

Can splash screens hurt startup performance?

Yes, if you preload too much behind them. Each asset you bundle into the splash-handler increases time-to-real-content. Limit preloads to what's strictly required for the first screen (auth state, primary fonts, hero image) and lazy-load everything else in the background after the home screen mounts.

Carlos Mendoza
About the Author Carlos Mendoza

Mobile performance engineer who profiles for a living. Has spent more hours in Flipper than he'll admit.