Code Splitting with React.lazy
In this page:
Splitting Code with React.lazy
React.lazy() takes a function that calls the dynamic import() syntax, returning a Promise for that component's module. The bundler (Vite, webpack) automatically splits that component into its own separate file, only downloaded when it's actually needed at runtime.
Note: This is most valuable for large components or entire routes not needed on the very first page load, like a settings page or an admin panel.
Warning: React.lazy's imported function must resolve to an object with a default export — a named export alone won't work directly with React.lazy.
Example: Splitting Code with React.lazy
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const LazySettings = React.lazy(() =>
Promise.resolve({ default: () => <p>Settings page (loaded on demand)</p> })
);
function App() {
const [show, setShow] = React.useState(false);
return (
<div>
<button onClick={() => setShow(true)}>Load Settings</button>
{show && <React.Suspense fallback={<p>Loading settings...</p>}><LazySettings /></React.Suspense>}
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Lazy Loading Routes
One of the most common and impactful uses of React.lazy is per-route code splitting: each page of your app becomes its own chunk, so a user visiting only the homepage never downloads the code for the admin dashboard or settings page they never visit.
Note: Combine this with React Router's Route element prop — each lazy-loaded page component still just plugs in normally.
Warning: Every lazy-loaded route needs to be wrapped in Suspense somewhere in the tree (often once, around the whole Routes block) — forgetting it breaks navigation to that route.
Example: Lazy Loading Routes
// Run in your local React project (npm install required)
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function AppRoutes() {
return (
<Suspense fallback={<p>Loading page...</p>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
⚠️ This example uses an npm package with no CDN build available here — run this in your local React project.
Weighing the Loading Flash Tradeoff
Lazy loading trades a smaller initial bundle for a brief loading state the first time a particular chunk is needed. For components that are ALWAYS needed right away (like the main navigation), this tradeoff isn't worth it — the loading flash adds friction with no real bundle-size benefit for that specific piece.
Note: Reserve lazy loading for things NOT needed immediately on first render — always-visible layout pieces should stay in the main bundle.
Warning: Lazy-loading something that's always immediately visible just adds an unnecessary loading flicker with no actual performance win.
Example: Weighing the Loading Flash Tradeoff
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
// Good candidate: rarely visited, large feature
const AdminPanel = React.lazy(() => Promise.resolve({ default: () => <p>Admin</p> }));
// Poor candidate: always visible immediately -- keep this in the main bundle
function MainNav() { return <nav>Always here, no lazy loading needed</nav>; }
function App() { return <MainNav />; }
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
- Forgetting React.lazy must always be paired with a Suspense boundary wrapping it, or it throws an error.
- Lazy-loading components that are always needed immediately (like the main layout), adding an unnecessary loading flash for no benefit.
- The dynamic import() function must return a module with a default export — React.lazy doesn't support named exports directly.
- React.lazy(() => import('./Component')) splits a component into its own separate JS chunk, loaded only when needed.
- This reduces the initial bundle size, improving the app's first-load performance.
- A lazy component must always be rendered inside a Suspense boundary with a fallback.
- Good candidates for lazy loading: routes, modals, and rarely-used features not needed on first render.
React.lazy available since React 16.6; relies on the bundler (Vite/webpack) supporting dynamic import().
Chapter Quiz — Complete all 14 topics to unlock
0/14 topics done
Complete these topics first:
- Error Boundaries
- React Portals
- Modals using Portals (practical use)
- React Suspense
- Code Splitting with React.lazy
- Introduction to Server Components
- Introduction to Next.js (server-side React)
- Using React with TypeScript
- Scalable Folder Architecture
- Common React Design Patterns
- Component Documentation with Storybook
- Accessibility (a11y) in React
- i18n with react-i18next
- React Security Best Practices