Lazy & Suspense — split the bundle

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ React.lazy is the code-splitting mechanism; the suspense_patterns bundle covers the loading mechanism — Suspense catches the promise that lazy throws.

static import → lazy import: pay for what you render

A static import Foo from './Foo' puts Foo's code in your main bundle — downloaded at first paint even if the user never opens Foo. React.lazy swaps that for a dynamic import(): Foo's code ships in a separate chunk that loads only when <Foo/> first renders. While the chunk streams, <Suspense fallback> paints a placeholder; once resolved the chunk is cached, so re-visiting is instant. Users download only the code for the view they're actually looking at.

pieceroleanalogy
React.lazy(load) wraps load → a component that suspends until the import resolves, reads .default the vending-machine button — you press it, the machine fetches the item on demand
load() returns a Promise (a dynamic import()); called once, result cached the order ticket — handed to the kitchen exactly once
<Suspense fallback> catches the thrown import promise, shows fallback, retries on resolve the "please wait" sign while the kitchen cooks
chunk the separate JS file your bundler emits for the lazy module the separately-shipped box — not in the main delivery truck

1 · the lazy component you write (edit me)

2 · Babel compiles JSX → element tree

<Suspense fallback={...}><LazyChart/></Suspense> becomes React.createElement(Suspense, {fallback:...}, React.createElement(LazyChart)). When <LazyChart/> renders, fakeLazy throws the pending promise — Suspense swaps in the fallback, then re-renders once it resolves.

// (hit "compile & render" to see Babel's output)

3 · live React (lazy + Suspense, proven)

The lazy component is not in the DOM until you click. Click "Load Lazy Component" — you'll see the Suspense fallback ("Loading chunk…"), then ~800ms later the chunk "resolves" and "Chart loaded lazily!" paints. The gold-check does this automatically: asserts nothing is loaded, clicks, catches the fallback, waits for resolution, asserts content.

phase: · gold: not-loaded → click → fallback → resolved "Chart loaded lazily!"

intent → pattern

intentpatternwhy
split a route/widget const X = lazy(() => import('./X')) + <Suspense fallback> code downloads on first render, not at boot
named-export module re-export as default: export { Named as default } lazy reads .default only — named exports break it
isolate a slow region nested <Suspense> around the lazy subtree only nearest boundary wins — the rest stays interactive
fail gracefully wrap <Suspense> in an <ErrorBoundary> a rejected chunk throws to the boundary, not past Suspense
warm the chunk early const p = import('./X') on hover/focus; render <LazyX/> after preload hides the network cost behind user intent