- React
- Performance Engineering, Profiling, and Virtualization
- Code Splitting with React.lazy & Suspense
Code Splitting with React.lazy & Suspense
Code splitting breaks monolithic JavaScript bundles into smaller chunks loaded on demand. React.lazy enables dynamic import() of components, while <Suspense fallback={<Spinner />}> coordinates loading boundaries during network chunk resolution.
Key Learning Objectives
Implement route-based and interaction-based code splitting using React.lazy and dynamic import().
Wrap lazy components inside <Suspense> boundaries with accessible fallback skeletons.
Handle network chunk load failures gracefully using Error Boundaries.
Implement route chunk prefetching on link hover to minimize perceived latency.
The Interview Problem & Context
Implement route-based code splitting using React.lazy, Suspense, and an Error Boundary to handle chunk loading failures.
1import React, { lazy, Suspense } from 'react';23// 1. Dynamic import generates a separate JS chunk bundle4const DashboardAnalytics = lazy(() => import('./DashboardAnalytics'));56export function AnalyticsPage() {7 return (8 <div className="page-container">9 <h1>Analytics</h1>10 {/* 2. Suspense renders fallback UI while chunk downloads */}11 <Suspense fallback={<div className="skeleton">Loading Analytics...</div>}>12 <DashboardAnalytics />13 </Suspense>14 </div>15 );16}
Deep Architectural Explanation
Code Splitting Architecture
A single large JavaScript bundle (e.g. 2MB) hurts Initial Server Response and Largest Contentful Paint (LCP) because the browser must download, parse, and compile all code before rendering.
1. How React.lazy Works
React.lazy takes a function that returns a Promise resolving to a module with a default export:
const HeavyChart = lazy(() => import('./HeavyChart'));- Build tools (Webpack, Rollup, Vite) recognize
import()and extract that component into a separate.jschunk file. - The chunk is only fetched over the network when the component is first rendered!
2. The <Suspense> Contract
Whenever a lazy component is evaluated, it throws a Promise internally.
<Suspense fallback={<LoadingSpinner />}>catches this thrown Promise.- It renders the
fallbackUI until the Promise resolves. - Once resolved, it seamlessly renders the real component.
3. Handling Chunk Load Failures (Deployment Stale Chunks)
If you deploy a new version of your app, old chunk filenames on the CDN will return 404s.
Always wrap Suspense boundaries in an Error Boundary that prompts the user to refresh the page!
Common Interview Traps & Pitfalls
Declaring React.lazy inside the body of a component (it must be declared at the module top level).
Forgetting the <Suspense> wrapper, causing React to crash when a lazy component mounts.
Over-splitting tiny components into hundreds of micro-chunks, introducing HTTP request waterfall overhead.
Senior Interviewer Follow-Up Probes
How do you prefetch a lazy-loaded chunk when a user hovers over a navigation link?
How does SSR handle React.lazy without flash of unstyled content?
How does Suspense for code splitting compare to Suspense for data fetching in React 19?
Frequently Asked Interview Questions
Where must React.lazy components be declared?
At the top level of the module outside of component render functions, ensuring the dynamic import is only initiated once.
What happens if a user navigates to a lazy route while offline?
The chunk fetch fails and the promise rejects. If wrapped in an Error Boundary, a friendly recovery UI or reload prompt will display instead of a white screen crash.
Can you use named exports with React.lazy?
React.lazy expects a default export. For named exports, you can chain the promise: lazy(() => import("./module").then(m => ({ default: m.NamedComponent }))).
How do you prefetch a lazy component before the user clicks?
By triggering the import() call on link hover or route focus: const loadComponent = () => import("./HeavyComponent"); onMouseEnter={loadComponent}.
