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.
| piece | role | analogy |
|---|---|---|
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
| intent | pattern | why |
|---|---|---|
| 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 |