Runnable lesson · press Try it on the examples to edit and run them here.
A user who opens your home page does not need the code for the admin screen, the chart library or the rich-text editor. Lazy loading (code splitting) downloads that code only when it is needed, so the first load is smaller and faster.
The mechanism
- Dynamic
import()loads a JavaScript module on demand and returns a promise. Bundlers such as Vite turn eachimport()into a separate file (a chunk). React.lazy(() => import("./Heavy.jsx"))wraps that promise as a component.<Suspense fallback={...}>shows a placeholder while the chunk downloads.
import { lazy, Suspense } from 'react'
const Reports = lazy(() => import('./Reports.jsx')) // not downloaded yet
function App() {
return (
<Suspense fallback={<p>Loading reports...</p>}>
<Reports />
</Suspense>
)
}
The module must export default the component (for named exports, wrap with .then((m) => ({ default: m.Reports }))).
Try it: simulated chunk loading
The playground has no separate files, so we simulate a slow chunk with a promise. Everything else, lazy, Suspense and the fallback, is the real thing:
function HeavyChart() {
return <p>Chart loaded (pretend this came from a big library)</p>;
}
// Stand-in for: lazy(() => import("./HeavyChart.jsx")), with a delay to mimic the network
const LazyChart = React.lazy(
() => new Promise((resolve) => setTimeout(() => resolve({ default: HeavyChart }), 900))
);
function App() {
const [show, setShow] = React.useState(false);
return (
<div>
<h3>Dashboard</h3>
<button onClick={() => setShow(true)}>Show chart</button>
{show && (
<React.Suspense fallback={<p>Loading chart...</p>}>
<LazyChart />
</React.Suspense>
)}
</div>
);
}
Nothing about the chart is loaded until you click. The fallback appears while lazy‘s promise is pending, then the component replaces it. (A lazy component’s promise is cached, so showing it a second time is instant.)
Where to split
Good candidates are large pieces that most visits do not use:
| Split by | Example |
|---|---|
| Route (the most common) | Each page is a lazy import |
| Rarely used feature | An editor, a chart, a PDF viewer, a map |
| Modals and tabs not yet opened | Settings dialogs |
| Heavy libraries | Import them inside the lazy component |
Do not split tiny components. Every chunk costs a network request, and over-splitting hurts.
A verified build
This small project was built with Vite, and the build was checked: it produced more than one JavaScript file, and the lazy component ended up in its own chunk.
function HeavyPanel() {
return <p>The heavy panel has loaded</p>
}
export default HeavyPanel
import { lazy, Suspense, useState } from 'react'
const HeavyPanel = lazy(() => import('./HeavyPanel.jsx'))
function App() {
const [show, setShow] = useState(false)
return (
<div>
<h1>Lazy loading demo</h1>
<button onClick={() => setShow(true)}>Load the panel</button>
{show && (
<Suspense fallback={<p>Loading...</p>}>
<HeavyPanel />
</Suspense>
)}
</div>
)
}
export default App
You can see it yourself: run npm run build and look at the dist/assets folder, or open the browser’s Network tab and click the button.
Lazy routes
The best place for lazy loading is the router. In data mode, a route can use lazy: () => import("./pages/Reports.jsx") (Route Loaders and Lazy Routes). With declarative routes, wrap the lazy pages in one <Suspense>:
const Reports = lazy(() => import('./pages/Reports.jsx'))
<Suspense fallback={<p>Loading...</p>}>
<Routes>
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
Preloading
To avoid a delay on click, start loading before the user asks: on hover, on focus or when the browser is idle. Call the import function early (import("./Reports.jsx")); the module is then already cached when lazy needs it.
Handle failure
A chunk can fail to load (offline, or you deployed a new version and the old filename is gone). Wrap lazy components in an error boundary (Error Boundaries) with a “Reload” button.
Key points
lazy(() => import("./X.jsx"))plus<Suspense fallback>downloads code only when needed.- Split by route and by large, rarely used features; do not split tiny components.
- Add an error boundary for failed chunks, and preload when you can predict the click.