lazy and Suspense Boundaries

lazy turns a dynamic import into a component. You hand it a function returning a promise for a module with a default export; React 7,897 calls it the first time the component renders, suspends until it settles, then renders the default export and remembers it. Every lazy component needs a <Suspense> boundary above it to own the waiting state — suspending is covered in Suspense; what code splitting adds is that the thing awaited is an undownloaded script.

Two lazy routes behind one boundaryJavaScript
import { lazy, Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary.jsx';
const Reports = lazy(() => import('./Reports.jsx'));        // module scope, not inline
const Customers = lazy(() => import('./Customers.jsx'));
export default function Panel({ page }) {
  return (
    <ErrorBoundary fallback={<p>That section failed to load. Reload the page.</p>}>
      <Suspense fallback={<p>Loading...</p>}>
        {page === 'reports' ? <Reports /> : <Customers />}
      </Suspense>
    </ErrorBoundary>
  );
}

Wrapping the sample application's two heavy routes this way shrank the entry chunk from 603.28 kB to 257.84 kB — 182.70 kB to 81.95 kB gzipped — with the chart moving into a 336.44 kB chunk most visitors never request.

Two rules keep this working. Declare lazy components at module scope — calling lazy() inside a component body creates a new component type on every render, which unmounts and refetches the subtree. And pair the boundary with an error boundary (Error Boundaries): Suspense does not catch rejections, so one dropped download would otherwise unmount the tree.