Code Splitting and Suspense
Every component built so far has been bundled into the app's JavaScript from the very first page load, regardless of whether it's ever used in a given visit. As an app grows — more pages, more rarely-used features, larger third-party libraries (a charting library, a rich text editor) — that single bundle keeps growing too, even for a user who only ever visits one page. Code splitting breaks the bundle into smaller pieces, loaded on demand; Suspense is what shows something reasonable on screen while a piece is still being fetched.
React.lazy — Loading a Component on Demand
lazy(() => import("./SettingsPage")) doesn't load SettingsPage's code at all until the moment it's actually about to be rendered for the first time — the dynamic import() call kicks off a separate network request for just that component's code, fetched as its own small bundle ("chunk") rather than being part of the main one. While that chunk is loading, the nearest wrapping <Suspense fallback={...}> shows its fallback instead of the real component, swapping over automatically the instant loading finishes.
lazy() expects the dynamically imported module to have a default export — import("./SettingsPage") resolves to an object, and lazy specifically looks for that object's .default property. A module exporting only a named export (export function SettingsPage() {...}, no export default) needs a small wrapper module re-exporting it as default, or needs to be changed to a default export directly.
Route-Based Splitting — The Most Common Use Case
Pairing lazy with the routes from Intermediate Chapter 5 is the single most common pattern: a user visiting / only ever downloads HomePage's code, SettingsPage's chunk never loads at all unless that user actually navigates to /settings. One Suspense boundary wrapping the whole Routes block is usually enough — every route shares the same fallback while its own page's chunk loads.
Loading a Rarely-Used Feature on Click
Here, AnalyticsChart's code (likely a sizeable charting library) doesn't load on the initial page visit at all — only once showChart becomes true, the conditional rendering from Fundamentals Chapter 5 means React only tries to render <AnalyticsChart /> (triggering its chunk to load) at the exact moment a user actually wants it.
Nesting Suspense Boundaries
A single component can be wrapped in its own dedicated Suspense, separate from a larger one further up — useful when one part of a page should show its own specific loading state rather than blocking everything else on the page behind one shared fallback. There's a real tradeoff either way: fewer, broader boundaries mean simpler fallback UI but more of the page "waits together"; more, narrower boundaries let unrelated parts of a page appear independently, at the cost of more fallback states to design.
lazy "just in case" creates a waterfall of tiny network requests, often slower overall than one slightly larger bundle loaded once. Code splitting earns its keep at natural boundaries — whole routes, and genuinely large or rarely-used features (a chart library, a rich text editor, an admin-only panel) — not for ordinary, frequently-used, lightweight components.
Suspense beyond just component code, letting a data-fetching hook also "suspend" rendering until its data arrives, using the same Suspense boundary and fallback shown here. The component-code-splitting pattern covered in this chapter is the stable, well-established baseline either way — worth knowing this direction exists, without needing to chase it immediately.
Coding Challenges
Build a component lazy-loaded with React.lazy, wrapped in a Suspense boundary with a visible fallback ("Loading..."), and confirm in the browser's network tab that its code arrives as a separate request rather than being part of the main bundle.
📄 View solutionBuild a small multi-page app (using React Router from Intermediate Chapter 5) where every page component is lazy-loaded, with one shared Suspense boundary wrapping the Routes block.
📄 View solutionBuild a Dashboard with a button that, only when clicked, reveals a lazy-loaded component wrapped in its own Suspense boundary — confirming the lazy component's code is not requested at all until the button is actually clicked.
📄 View solutionChapter 3 Quick Reference
- lazy(() => import("./X")) — loads a component's code only when it's first rendered
- <Suspense fallback={...}> — shows the fallback while a wrapped lazy component's chunk is loading
lazyrequires a default export from the imported module- Most common use: route-based splitting, pairing
lazywith React Router (Intermediate Ch 5) - Also useful for rarely-used features loaded only on demand (a click, a conditional render)
- Don't lazy-load everything — apply it at route boundaries and genuinely large/rare features, not small everyday components
- Next chapter: testing — Jest and React Testing Library