Improving Startup Speed with React Navigation Lazy Screen Loading
Learn how to defer screen mounting in React Navigation to reduce initial render work, with a concrete example and practical trade‑offs.
01 Aug 2025, 11:28 UTC

Problem: Slow app start when many screens are defined up front
When a React Native app uses a stack, tab, or drawer navigator with many screens, React Navigation mounts every screen component during the first render. Even if the user never visits a particular screen, its component tree, state initializations, and any side effects in useEffect or constructors run immediately. This can add noticeable delay to the splash screen, especially on lower‑end devices.
Thesis: Lazy screen loading defers mounting until the screen gains focus, cutting initial work while preserving navigation behavior
By marking a screen with lazy: true, React Navigation postpones instantiation of that screen’s component until the user navigates to it. The screen then mounts, receives focus events, and unmounts when it loses focus (if the navigator is configured to unmount inactive screens). The trade‑off is a small latency the first time the screen is opened, because its mount work happens then instead of at startup.
How to enable lazy loading
Lazy loading can be set globally for a navigator or per‑screen. It works with the three most common navigators:
@react-navigation/native-stack@react-navigation/bottom-tabs@react-navigation/drawer
To enable it globally, add lazy: true inside screenOptions. To enable it for a single screen, pass an options prop with { lazy: true }.
Example: Global lazy loading in a bottom tab navigator
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import HomeScreen from './HomeScreen';
import ProfileScreen from './ProfileScreen';
import SettingsScreen from './SettingsScreen';
const Tab = createBottomTabNavigator();
export default function MainTabs() {
return (
);
}
Place this code in the root of your app (e.g., App.js or a dedicated navigator file). No special permissions are required; it runs with the same privileges as the rest of your React Native code.
Worked example: Verifying that a lazy screen mounts only on first focus
We’ll add a simple log to the second tab to prove that mounting is deferred.
Step‑by‑step
- Create a fresh Expo project (or use an existing one) and install React Navigation if not already present:
# Run in your project directory
npx expo install @react-navigation/native @react-navigation/bottom-tabs
npx expo install react-native-screens react-native-safe-area-context
- Add the navigator from the previous example to
App.js. - Modify the
ProfileScreencomponent to log when it mounts and when it gains focus:
import React, { useEffect } from 'react';
import { View, Text } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
export default function ProfileScreen() {
useEffect(() => {
console.log('ProfileScreen mounted');
}, []);
useFocusEffect(
React.useCallback(() => {
console.log('ProfileScreen focused');
return () => {
console.log('ProfileScreen blurred');
};
}, [])
);
return (
Profile Screen
);
}
- Run the app (
npx expo start) and open the debugger or useadb logcatto view console output. - Observe that the first render (when the app starts) only logs mounting for the
Homescreen (the initial tab). No "ProfileScreen mounted" appears yet. - Tap the Profile tab. You should now see "ProfileScreen mounted" followed by "ProfileScreen focused". Subsequent taps to the Profile tab produce only the focus/blur logs, confirming the screen stays mounted while focused.
Risk: If the screen performs heavy work (e.g., large data fetches) directly in the render phase or in a useEffect without proper dependencies, the first navigation to that screen may cause a noticeable frame drop. Move expensive initialization out of render and into useEffect with an empty dependency array, or consider a loading indicator while the work completes.
Trade‑off and limitation
The primary trade‑off is the latency incurred the first time a lazy screen is opened. While the initial app launch is faster, the user may experience a brief pause when navigating to a screen that has never been mounted before. This latency is proportional to the amount of work the screen does during mount.
Another limitation involves layout measurements. Because a lazy screen is not mounted until it receives focus, any attempt to read dimensions via measure, useWindowDimensions, or LayoutAnimation in the render phase will return zero or undefined. Place such code inside useEffect or useFocusEffect to guarantee the layout exists.
Practical way to check the result
To verify that lazy loading improves startup time:
- Build a production‑like bundle (e.g.,
npx expo run:android --variant releaseor the iOS equivalent). - Use a profiling tool such as Android Studio’s Profiler or Xcode’s Instruments to record the time from app start to the first frame rendered.
- Compare two builds: one with
lazy: trueremoved (or set tofalse) and one with the setting enabled. The enabled build should show a smaller “time to first frame” value. - Additionally, enable the React DevTools Profiler, record a session while navigating from the home screen to a lazy screen, and inspect the commit times. The first commit for the lazy screen will appear only after navigation.
Actionable closing
If your React Native app suffers from a slow start because many screens are defined up front, try enabling lazy loading on the navigator(s) that contain infrequently used screens. Start with a global lazy: true setting, then measure the impact on launch time and on the first‑navigation latency. Adjust by moving heavy initialization out of render and adding loading indicators where needed. This simple configuration can shave hundreds of milliseconds off startup without sacrificing the full navigation experience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.