Ch. 3 · React

React lazy Loading and Code Splitting

Split bundles with lazy and Suspense, work with default exports, and place boundaries so a chunk loads only when needed.

~2 min readintermediateupdated Oct 5, 2026

React.lazy defers loading a component’s module until it is first rendered, which moves that code into a separate chunk. Wrapped in Suspense, the component shows a fallback while the chunk downloads, so the initial bundle stays small.

Before you start

You should be comfortable with components, dynamic import() and Suspense. This article covers client-side splitting; server rendering has additional constraints.

Step-by-step walkthrough

Step 1: Wrap the import in lazy and a Suspense boundary

const Chart = lazy(() => import('./Chart')) creates a component whose module loads on demand. Render it inside <Suspense fallback={...}> so the tree has something to show while the promise resolves; without a boundary, the nearest parent boundary catches it or the app crashes.

Step 2: Match the export shape

lazy expects a module whose default export is the component. For a named export, map it: lazy(() => import('./Chart').then((m) => ({ default: m.Chart }))). Forgetting this produces a runtime error about an invalid component type.

Step 3: Split at meaningful boundaries

Route-level splitting gives the biggest win: the initial route loads alone and other routes arrive on navigation. Component-level splitting suits heavy, below-the-fold widgets. Avoid splitting tiny components, where the extra request costs more than the saved bytes.

Worked scenario

The chart chunk loads only when the component first renders.

import { lazy, Suspense } from 'react';

const Chart = lazy(() => import('./Chart'));

export function Dashboard() {
  return (
    <Suspense fallback={<p>Loading chart…</p>}>
      <Chart data={[1, 2, 3]} />
    </Suspense>
  );
}
TSX

Walk through the example

The initial bundle contains Dashboard but not Chart. When Dashboard renders, React sees the lazy component, starts the dynamic import, and shows the fallback until the promise resolves, then renders the chart. Navigating away before it loads cancels nothing harmful, and a later render reuses the already-loaded module.

Common mistake

Calling lazy inside a component body, which recreates the lazy component on every render and remounts the subtree. Define it at module scope. Another is expecting lazy to load on hover or in the background; it loads when first rendered unless you prefetch the import.

Verify the behavior

Build and inspect the output for a separate chunk for the lazy component. Confirm the chunk is not requested on first paint and appears when the component renders. Delay the network and check the fallback shows, then confirm the component renders once the chunk arrives.

Interview exercise

Where should the Suspense boundary go for a lazy route?

Answer and reasoning

Around the rendered route, so the fallback replaces the route content while its chunk loads and the surrounding layout stays mounted. A boundary high in the tree would blank the whole app, and a boundary with no fallback would show nothing. Route-level boundaries also let a slow chunk fail or fall back without taking down navigation.

Continue learning

Compare pending UI in Suspense boundaries and Error boundary scope. Read the React lazy reference and try the React interview questions.

More in React

read ✓React · hard

React Context Splitting for Performance

Stop a single context value from re-rendering every consumer by splitting state and dispatch and memoizing the provider value.

~2 min readread →
read ✓React · mid

React Portals and Event Bubbling

Render outside the parent DOM with createPortal while events keep bubbling through the React tree, and manage focus and z-index.

~2 min readread →
esc