How would you lazy load components with React.lazy and Suspense inside protected routes
`React.lazy(() => import('./Page'))` defers the chunk; wrap with `<Suspense fallback={<Skeleton />}>`. Compose with route protection: check auth first, then lazy-render the protected component. Use `<ErrorBoundary>` to handle chunk-load failures (network blip). Prefetch on link hover to mask first-visit latency.
Basic shape
const Dashboard = React.lazy(() => import("./Dashboard"));
function App() {
return (
<Suspense fallback={<Skeleton />}>
<Dashboard />
</Suspense>
);
}React.lazy returns a component that, on first render, triggers the dynamic import; Suspense shows the fallback while the chunk loads.
Inside a protected route
const AdminPanel = React.lazy(() => import("./AdminPanel"));
function ProtectedRoute({ role, children }: { role: string; children: ReactNode }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" replace />;
if (!user.roles.includes(role)) return <Forbidden />;
return <Suspense fallback={<Skeleton />}>{children}</Suspense>;
}
<Route path="/admin" element={<ProtectedRoute role="admin"><AdminPanel /></ProtectedRoute>} />The auth check runs before the lazy chunk is requested — denies unauthorized users without downloading the admin bundle.
Handle chunk-load failures
A flaky network or a stale deploy can fail the dynamic import. Wrap with an error boundary:
class ChunkBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) { return { error }; }
componentDidCatch(err) {
if (err.name === "ChunkLoadError") {
// Deploy mismatch — full reload picks up the new manifest
window.location.reload();
}
}
render() { return this.state.error ? <Fallback /> : this.props.children; }
}ChunkLoadError almost always means "user has stale HTML referencing chunks from a previous deploy" — reloading fixes it.
Prefetch on hover
function LinkPrefetch({ to, importer, children }) {
const onHover = useCallback(() => importer(), [importer]);
return <Link to={to} onMouseEnter={onHover} onFocus={onHover}>{children}</Link>;
}
<LinkPrefetch to="/admin" importer={() => import("./AdminPanel")}>Admin</LinkPrefetch>By the time the user clicks, the chunk is in the browser cache. Next.js <Link> does this automatically.
Nested Suspense
You can nest Suspense boundaries so different chunks resolve at different times:
<Suspense fallback={<Skeleton />}>
<Sidebar />
<Suspense fallback={<MainSkeleton />}>
<Dashboard />
</Suspense>
</Suspense>Sidebar renders as soon as its chunk + data are ready; Dashboard streams in next.
When NOT to lazy load
- Above-the-fold content that's needed for first paint.
- The login page itself (otherwise users see blank → skeleton → login).
- Small components — chunk overhead outweighs benefit.
Server-side considerations
In Next.js App Router, dynamic imports work in both server and client components. next/dynamic adds { ssr: false } for client-only modules.
Combining with role-based code splitting
For role-gated features, don't ship the chunk at all to unauthorized users. Achieve this by:
- Lazy import only inside the protected branch (above pattern).
- Or split bundles per role at the server, only serving the bundle the user has access to.
Interview framing
"React.lazy(() => import(...)) for the chunk, wrap with <Suspense fallback={...}> for the loading UI. For protected routes, do the auth/role check before rendering the lazy component — unauthorized users don't trigger the download. Wrap with an error boundary that handles ChunkLoadError by reloading (almost always means stale HTML after a deploy). Prefetch on hover via onMouseEnter={() => import(...)} to mask first-visit latency — Next.js <Link> does this for free. Nest Suspense to stream different sections. Don't lazy-load the login page or above-the-fold content."
Follow-up questions
- •Why prefetch on hover?
- •How would you handle a chunk that fails to load?
- •When is lazy loading the wrong choice?
Common mistakes
- •Lazy-loading above-the-fold content.
- •No error boundary around lazy components.
- •Shipping the chunk regardless of auth.
Performance considerations
- •Per-route splitting wins LCP; prefetch hides cold visit latency.
Edge cases
- •Stale-deploy ChunkLoadError.
- •Suspense fallback flash on fast networks.
- •SSR with client-only lazy components.
Real-world examples
- •React Router lazy loaders, Next.js dynamic, Remix route splitting.