Code Splitting in React with lazy() and Suspense

Split a React bundle with lazy and Suspense so a component downloads only when it first renders, with a fallback while the chunk loads.

6 min read

Code splitting in React breaks a large JavaScript bundle into smaller chunks that load only when they are needed. The lazy and Suspense pair is how you split a component's code so it downloads on first render instead of with the initial page.

Why split the bundle

A growing app ships as one large JavaScript file by default. Users wait for all of it before the first paint, even for screens they never open.

Code splitting lets a feature's code arrive later, when the user actually reaches it. Heavy features such as a chart editor or a markdown preview are the best candidates.

A markdown preview can pull in a full parser, adding tens of kilobytes that readers do not need when the preview is hidden. Splitting it removes that weight from the initial download.

Defer a component with lazy

The lazy function wraps a dynamic import so the component loads on demand. It relies on your bundler's support for dynamic import(), which Vite, webpack, and most current tools provide.

The dynamic import returns a promise, and the bundler turns the requested module into a separate chunk file. lazy resolves the promise's default property as the component to render.

App.jsxApp.jsx
import { lazy } from "react";
 
const MarkdownPreview = lazy(() => import("./MarkdownPreview.js"));

The imported module must have a default export. Call lazy at module top level, outside your components. Declaring it inside a component re-creates it on every render, which resets the component's state.

Show a fallback with Suspense

While the chunk downloads, React suspends the component. Wrap it in Suspense and supply a fallback.

App.jsxApp.jsx
import { lazy, Suspense } from "react";
 
const MarkdownPreview = lazy(() => import("./MarkdownPreview.js"));
 
export default function Preview({ markdown }) {
  return (
    <Suspense fallback={<p>Loading preview...</p>}>
      <MarkdownPreview markdown={markdown} />
    </Suspense>
  );
}

The first time Preview renders, React requests the MarkdownPreview chunk and shows the fallback paragraph. When the chunk resolves, the fallback is replaced by the real preview. Later renders reuse the cached component, so the fallback never appears again.

Keep the fallback small so it appears instantly. A spinner or a skeleton works better than a full loading page, because the fallback is the only thing on screen while the chunk downloads.

Loading a lazy component on demand

The diagram shows the one-time path of a lazy component. The request happens only on the first render, which is why the fallback appears once and then never again.

Where to place boundaries

Put a Suspense boundary around a whole section, not around every component. Boundaries define your loading sequence, so they should match the granularity the user actually sees. A page-level boundary shows one spinner, while nested boundaries reveal sections progressively.

A common pattern is to render the lazy component only after the user enables it, such as ticking a Show preview checkbox. The first tick triggers the download, and later ticks reuse the cached chunk.

Do not use a Suspense boundary for data fetched in an Effect or event handler. Suspense only activates for lazy code, a Promise read with use, or a Suspense-enabled framework's data source.

What lazy requires

  • A default export in the lazy module.
  • A module-top-level declaration.
  • A bundler that supports dynamic import().
  • In a framework with Server Components, lazy applies to Client Components, while the framework splits Server Components itself.

Keep navigation smooth with transitions

A lazy component that suspends during navigation can flash its fallback over already visible content. Marking the navigation update as a transition keeps the current page on screen while the new chunk loads.

See useTransition for that pattern, and React Compiler for the build-time optimizations that pair with code splitting. Confirm the bundle was worth splitting with the measure-first workflow.

Rune AI

Rune AI

Key Insights

  • lazy defers a component's code until it is first rendered.
  • Suspense shows a fallback while that chunk loads.
  • lazy requires a default export and a module-top-level declaration.
  • Place boundaries around whole sections, not every component.
  • Transitions keep already visible content from hiding during navigation.
RunePowered by Rune AI

Frequently Asked Questions

Does lazy work with named exports?

lazy expects a module with a default export. If a module only has named exports, re-export the component as the default from an intermediate file.

Where should I declare lazy components?

At the top level of a module, never inside another component. Declaring one inside a component re-creates it on every render and resets its state.

Conclusion

lazy and Suspense split a component's code out of the main bundle and load it on first render. Declare lazy components at module top level, give Suspense a lightweight fallback, and place boundaries only where a loading sequence makes sense.