When a user taps your app icon from the home screen, you have roughly 1.2 seconds before their brain registers a delay.
If your splash screen stays frozen for three or four seconds, users perceive your app as bloated and unresponsive. On Android, if launch exceeds five seconds, the OS presents an “Application Not Responding” (ANR) dialog asking if they want to force close your app.
Here is the exact cold-start timeline breakdown we look for when profiling a mobile launch:
[OS Process Spawn] ──► [Native Framework Linking (dyld/linker)]
│ (120ms)
▼
[JS Runtime Initialization (Hermes / V8)]
│ (180ms)
▼
[Top-Level JavaScript Evaluation (require / import trees)]
│ (1,800ms ◄── 80% of teams blow their budget right here!)
▼
[Initial Screen Render & First Native Commit]
Notice that massive block in the middle? Top-Level JavaScript Evaluation.
The Crime: Evaluating Everything at Boot
In modern JavaScript and TypeScript codebases, developers write top-level import statements without thinking about when that code actually needs to execute:
// App.tsx
import React from 'react';
import { View } from 'react-native';
import { HeavyAnalyticsEngine } from './analytics/HeavyAnalyticsEngine'; // 240ms to parse
import { PDFViewerModal } from './components/PDFViewerModal'; // 180ms to parse
import { CreditCardScanner } from './checkout/CreditCardScanner'; // 320ms native init
import { MainNavigation } from './navigation/MainNavigation';
When the Hermes engine loads your bundle at startup, it has to evaluate every single one of those imported files immediately, long before the user has even logged in or tapped the checkout button.
Why are we initializing a Credit Card Scanner native module when the user is simply opening their home feed?
Fix #1: Enable Metro Inline Requires
React Native’s Metro bundler has a built-in weapon that most teams forget to configure: Inline Requires.
Instead of eagerly executing modules at file load time, Metro transforms imports so modules are only required at the exact moment their function or class is first called:
// metro.config.js
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true, // Defers module execution until call time!
},
}),
},
};
Enabling this single boolean switch often chops 800ms off cold start overnight.
Fix #2: Defer Non-Critical SDKs Past the First Render
If you are initializing Sentry, Segment, Intercom, and OneSignal synchronously in your native AppDelegate or root index.js, you are blocking the first frame.
We defer all non-critical telemetry until after the first screen commits:
// index.js
AppRegistry.registerComponent(appName, () => RootApp);
// Run heavy SDK telemetry initialization during next idle microtask
requestIdleCallback(() => {
initAnalytics();
preloadSecondaryScreens();
});
The splash screen dismisses instantly, the user sees interactive content in under 800 milliseconds, and background telemetry boots smoothly without causing an ANR.