In React Native, why do eager screen imports and module side effects lengthen cold start, and how do lazy-loaded screens help?
answer
- one bundle already on the device
- import evaluates the whole module graph
- top-level code runs at require time
- getComponent with an inline require
- lazy saves evaluation, not a download
basics
~20 sEvery module imported from the entry file is evaluated before the first screen renders, including any top-level work it does. Loading rarely used screens lazily, with getComponent or React.lazy, keeps that work off the startup path.
solid answer
~50 sIn a React Native release build the whole app is one bundle on the device, so startup cost is not downloading code but **evaluating** it: each statically imported module runs its top-level code before the entry can render, and so does everything it imports. A root navigator that imports every screen evaluates checkout, maps and settings code at launch. **Module side effects** make it worse - SDK initialisation, building an index from a bundled JSON file, patching globals - because they run at import time for every launch. Load screens lazily: React Navigation's `getComponent={() => require('./CheckoutScreen').default}` evaluates the module the first time that screen renders, and `React.lazy` does the same through `Suspense`. Move top-level work into functions called when needed. The React Native docs warn that lazy loading can change behaviour if a module relied on a side effect running early, so remove those side effects first.
code
tsx · 25 linesimport { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from './screens/HomeScreen'; // first screen: needed at startup
type RootStackParamList = { Home: undefined; Checkout: undefined; StoreLocator: undefined };
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
{/* Module evaluated the first time the screen renders, not at launch */}
<Stack.Screen
name="Checkout"
getComponent={() => require('./screens/CheckoutScreen').default}
/>
<Stack.Screen
name="StoreLocator"
getComponent={() => require('./screens/StoreLocatorScreen').default}
/>
</Stack.Navigator>
</NavigationContainer>
);
}go deeper
Remember that everything imported from the entry file runs before the first screen appears, so importing every screen up front slows startup.
Explain module evaluation, why side effects run at import time, and how getComponent or React.lazy defer a screen's module until it first renders.
Show you can audit the startup import graph, remove load-bearing side effects before deferring modules, and prove the gain on a release build on mid-range Android.
Discuss conventions that keep startup lean as the app grows: lazy screens by default, no import-time side effects, and review of what the entry file pulls in.
## Where startup time goes on the JavaScript side A cold start runs native initialisation first, then loads the JavaScript bundle and runs it until the first screen is on screen and responds. On the JavaScript side, the dominant cost is usually **module evaluation**: - A React Native release build ships the app's JavaScript as **one bundle on the device**; on Android it is the `index.android.bundle` asset. There is no network download to save. - With Hermes, release builds contain precompiled bytecode that is loaded on demand, so parsing is cheap; but **running** module code is not. - A static `import` at the top of a file means the imported module's factory runs before the importing module finishes. Starting from the entry file, the whole reachable import graph is evaluated before React renders anything. So the question for startup is: **how much code runs before the first screen renders?** ## Two ways apps make that number large **1. Eager screen imports.** A root navigator file that imports twenty screens evaluates all twenty modules, and everything they import (maps SDKs, charting, video players), even though the user sees only the home screen. **2. Module side effects.** Anything at the top level of a module runs when the module is evaluated: | Side effect at import time | Startup cost | |---|---| | Initialising an analytics or payments SDK | native calls and setup on every launch | | Building a search index from a bundled catalog JSON | parsing and looping over thousands of items | | Registering global listeners or patching globals | work plus hidden ordering dependencies | | Reading storage synchronously to prefill a store | I/O before the first render | The React Native docs single out side effects for a second reason: once a module's side effect is load-bearing, **lazy loading changes behaviour**, because the code now runs later or not at all. ## Lazy-loaded screens The fix is to evaluate a screen's module only when the screen is first shown: - **React Navigation `getComponent`**: a screen can take `getComponent` instead of `component`. Returning `require('./screens/CheckoutScreen').default` from it means the module is evaluated the first time that screen renders. The home screen stays a static import because it is needed immediately. - **`React.lazy`**: `lazy(() => import('./CheckoutScreen'))` defers evaluation too, and needs a `Suspense` boundary for the moment the component is not ready. - **Tabs**: React Navigation's bottom tabs render a tab's screen the first time it is visited by default (`lazy` defaults to `true`), which keeps inactive tabs from rendering at launch. In React Native these techniques save **evaluation and rendering work**, not bytes over the network, which is why their payoff is measured in startup time rather than download size. ## Removing side effects safely 1. **Find them**: look for top-level calls in modules reachable from the entry file - `init(...)`, `new Something()`, loops, storage reads. 2. **Turn them into explicit functions**: `getCatalogIndex()` builds the index on first use; an `initAnalytics()` call happens in one known place, possibly after the first screen renders. 3. **Keep genuinely required setup explicit and early**, such as error handlers or polyfills, rather than hiding it in a module that might become lazy later. 4. **Measure again** on a release build on a mid-range Android device. ## Pitfalls - Making a screen lazy while another module still imports it statically, so it is evaluated at startup anyway. - Lazy-loading the first screen the user sees, which only adds a step. - Moving a side effect later without checking who depended on it, which creates bugs that appear only on some navigation paths. - Importing a large JSON file at the top of a commonly used module. - Assuming a smaller bundle alone fixes startup; what matters is how much of it **runs** before the first screen.
- A screen was switched to getComponent but startup did not improve. What would you check?Whether something reachable from the entry file still imports the screen statically, such as a barrel `index.ts` or a shared helper, which evaluates it at launch anyway. Also check that its heavy dependencies are not imported by the home screen.
- Why can lazy loading a module break behaviour elsewhere in the app?If the module had a side effect at import time - registering a listener or setting a global - other code may depend on that having run. Once the module is lazy, the effect happens later or never, so remove such side effects before deferring the module.
saying these in an interview costs you the question
- Lazy loading in React Native saves a network download of code
- Imported modules only cost time when their exports are used
- Top-level code in a module runs only in development
- The first screen should also be lazy-loaded to speed startup
- Bottom tabs render every tab's screen at launch by default