Using React.lazy and Suspense for component-level code splitting
React.lazy turns a dynamic import into a component; Suspense renders a fallback while the chunk loads. Together they split bundles at component granularity without ejecting from React's render model.
React.lazy(() => import('./Heavy')) returns a special component that, on first render, suspends until the chunk loads. <Suspense fallback={...}> declares what to show during that suspension.
Why it matters: shipping a single 500KB main bundle slows TTI for everyone. Splitting at routes (or heavy components like a chart library) means users only download what they need for the path they took.
Where to put boundaries:
- Route level — every route lazy-loaded behind a
<Suspense>(Next.js App Router does this automatically forpage.tsx). - Heavy widget level — a charting modal, an emoji picker, a markdown editor. Wrap each in its own boundary so a slow chunk doesn't blank the rest of the page.
- Above-the-fold things should NOT be lazy — paying a network round trip on first paint is worse than the bundle bytes.
Failure modes: a lazy import can fail (deploy in flight, network hiccup). Pair every Suspense boundary with an Error Boundary that offers a retry; React.lazy doesn't ship retry on its own.
Server-side: in Next.js prefer next/dynamic over React.lazy for SSR control (ssr: false for client-only widgets).
Code
Follow-up questions
- •Why does React.lazy require a default export?
- •When should you choose next/dynamic over React.lazy?
- •How do you preload a lazy chunk before the user navigates?
Common mistakes
- •Lazy-loading above-the-fold content and adding a network round trip to LCP.
- •Forgetting an error boundary — a failed chunk silently shows the fallback forever.
- •Splitting too aggressively, ending up with hundreds of tiny chunks (HTTP overhead beats download savings).
Performance considerations
- •Combine with `<link rel='modulepreload'>` to start the chunk download during idle time.
- •Use route-level prefetch (Next's `<Link prefetch>`) so the chunk is warm before the click.
Edge cases
- •Suspense without a boundary above throws — every lazy component needs one in its parent tree.
- •Strict Mode double-invokes the import in development — that's the framework, not a bug.
Real-world examples
- •An admin dashboard splits each feature module behind its own boundary; analysts who never open Reports never download the chart bundle.