Implement route‑level code splitting with React Router v6 using lazy loading and Suspense
Learn how to split React Router v6 routes into separate bundles with React.lazy and Suspense, reducing initial load size and improving performance.
20 Jul 2025, 22:33 UTC

Desired outcome
Load each route component only when the user navigates to it, which reduces the initial JavaScript bundle size and improves perceived performance.
Prerequisites
- React Router v6 installed (
npm i react-router-dom@6oryarn add react-router-dom@6). - React 16.8+ (hooks are required for
React.lazyandSuspense). - A bundler that supports dynamic
import()syntax (e.g., Webpack 4+, Vite, Rollup, or Parcel). - Basic familiarity with JSX and routing concepts.
Procedure
1. Create lazy components for each route
Use React.lazy to wrap a dynamic import of the component file. Place this in the file where you define your routes.
import { lazy } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
The import path (./pages/Home, etc.) is a placeholder; replace it with the actual location of your component.
2. Define routes with <Suspense> fallback
Wrap the <Outlet> (for nested routes) or individual <Route> elements in a <Suspense> component that shows a UI while the chunk loads.
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
import { Suspense } from 'react';
function App() {
return (
Loading…}>
} />
} />
}>
{/* nested routes could go here */}
/>
{/* optional catch‑all for 404 */}
Not found} />
);
}
export default App;
The fallback prop can be any React element; a spinner, skeleton, or simple message works.
3. (Optional) Add an error boundary for failed loads
If a chunk fails to load (e.g., network error), the <Suspense> boundary will throw. Wrap it in an error boundary to show a retry UI.
class RouteErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
handleRetry = () => this.setState({ hasError: false });
render() {
if (this.state.hasError) {
return (
Failed to load route.
Try again
);
}
return this.props.children;
}
}
function App() {
return (
Loading…}>
{/* routes as before */}
);
}
Expected checks
- Open Chrome DevTools → Network tab, enable “Disable cache”.
- Navigate to each lazy route (e.g., /about). Observe a new chunk request (named after the imported file) appear.
- Verify that the fallback UI (the “Loading…” div) is shown while the chunk is downloading, then replaced by the route component.
- Check the Console for no errors related to module loading.
- Optional: throttle network to Slow 3G to ensure the fallback remains visible for a perceptible time, confirming Suspense behavior.
Recovery options
- If a chunk fails to load, the error boundary from step 3 will display a retry button.
- Provide a fallback route (e.g., a generic error page) for paths that do not match any defined
<Route>. - Ensure your bundler is configured to generate separate chunks for each dynamic import (most do this by default; verify the output contains multiple files).
Limitations and practical verification
Lazy loading only works with bundlers that understand import() as a code‑split point. Static site generators that pre‑render HTML may need extra configuration to load chunks on the client. Avoid lazy‑loading libraries that rely on synchronous side effects at the top level of the module, as those side effects will not run until the chunk is fetched, potentially breaking the library.
To confirm the setup works in production, build the app (npm run build or equivalent), serve the output with a static server, and repeat the network checks. The fallback UI should still appear, and the separate chunks should be requested on navigation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.