Lazy Routes, Suspense and Loading Transitions
IntermediateSplitting by route is the highest-value code splitting there is: a visitor to the home page should not download the admin panel or the code editor.
Overview
Every route in a single bundle means the first visitor downloads the entire application before seeing anything. Route-level splitting is the natural boundary — each route becomes its own chunk, fetched when someone navigates to it. React.lazy and Suspense express this in a few lines. The details that separate a good implementation from a janky one are prefetching on intent so the chunk is already there when the click happens, and handling a chunk that fails to load after a deploy.
Splitting a Route
lazy plus Suspense, and what to put in the fallback.
const Admin = lazy(() => import('./features/admin/AdminPage'))
const Editor = lazy(() => import('./features/editor/EditorPage'))
<Route
path="/admin"
element={
<Suspense fallback={<PageSkeleton />}>
<Admin />
</Suspense>
}
/>
// Put the Suspense boundary inside the layout, not around it —
// otherwise the header and nav disappear during every transition
// and the whole page flashes.
// Worth splitting: admin areas, the code editor, chart libraries,
// rich text editors, PDF generation, anything most visitors never open.
// Not worth splitting: small components on the critical path — an
// extra request costs more than the bytes saved.
// With React Router's own lazy, the route module itself is split,
// loader included:
{ path: 'admin', lazy: () => import('./routes/admin') }Prefetching
Load the chunk on intent, so the navigation itself is instant.
// The chunk downloads when the user hovers or focuses the link —
// typically 200-300ms before the click lands
function PrefetchLink({ to, load, children }) {
const prefetch = () => { load() }
return (
<Link to={to} onMouseEnter={prefetch} onFocus={prefetch}>
{children}
</Link>
)
}
<PrefetchLink to="/admin" load={() => import('./features/admin/AdminPage')}>
// Or prefetch on idle for a route you know most users reach:
useEffect(() => {
const id = requestIdleCallback(() => import('./features/editor/EditorPage'))
return () => cancelIdleCallback(id)
}, [])
// useTransition keeps the current page interactive during the
// transition instead of dropping straight to a skeleton:
const [isPending, startTransition] = useTransition()
startTransition(() => navigate('/admin'))
{isPending && <TopProgressBar />}When a Chunk Fails
The deploy-time failure that looks like a broken app, and its fix.
// You deploy. Hashed filenames change and old chunks are removed.
// A user with the page still open clicks a lazy route:
// "Failed to fetch dynamically imported module"
// The app appears broken, and a refresh silently fixes it — which
// is why this often goes unreported and unfixed for months.
// Catch it and offer the reload
class ChunkErrorBoundary extends Component {
state = { failed: false }
static getDerivedStateFromError(error) {
return { failed: /dynamically imported module|Loading chunk/i.test(error.message) }
}
render() {
if (this.state.failed) return (
<div role="alert">
<p>A new version is available.</p>
<button onClick={() => location.reload()}>Reload</button>
</div>
)
return this.props.children
}
}
// Better still: keep old chunks available for a while after deploy,
// and detect a new build version to prompt a reload proactively.Key Points to Remember
- 1Route-level splitting is the highest-value split — visitors do not download features they never open
- 2Place the Suspense boundary inside the layout so the shell does not flash on every transition
- 3Prefetch a chunk on hover or focus so the navigation itself feels instant
- 4useTransition keeps the current page interactive instead of dropping immediately to a fallback
- 5After a deploy, old chunks disappear and lazy imports fail — catch that error and offer a reload
Interview Questions
Sign in to ask AriaWhat should and should not be code-split in a React app?
Why does placing Suspense around the whole layout cause a flash?
Why do dynamic imports fail after a deploy, and how do you handle it?
Ask Aria about Lazy Routes, Suspense and Loading Transitions
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.