Back to React
React
medium
very high
mid

How do you use 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.

6 min read·~12 min to think through

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 for page.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

tsx
const Settings = React.lazy(() => import("./Settings"));

<ErrorBoundary fallback={<RetryButton />}>
  <Suspense fallback={<Skeleton />}>
    <Settings />
  </Suspense>
</ErrorBoundary>
Route-level split with retryable boundary
tsx
function lazyWithRetry<T>(load: () => Promise<{ default: T }>) {
  return React.lazy(() =>
    load().catch(() => new Promise((res) => setTimeout(() => res(load()), 1000))),
  );
}
Retry an import that failed (network blip)

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.

Senior engineer discussion

Senior signal: discuss the split-by-route vs split-by-component tradeoff, prefetch strategy on hover/idle, and how RSC changes the bundle-split picture entirely (server components don't ship JS to begin with).

Related questions