Lesson 12 of 26
Code-Splitting and Bundle Budgets
Why one giant bundle is the wrong default, how route- and component-level splitting trade upfront load for later waterfalls, and setting a real budget.
Bundle size is one of the few performance levers a team controls almost entirely on its own — no CDN, no server tuning, no user's network conditions involved. It's also one of the easiest to let creep upward slowly, one "just add this library" decision at a time, until a formerly-fast app is shipping megabytes of JavaScript nobody visiting a given page actually needs.
The problem with one bundle
A single JS bundle containing every route, every feature, every rarely-used admin tool is simple to build and deploy, and it's wrong the moment an app has more than a couple of routes: every visitor pays the download, parse and execution cost for code they may never touch in this session. A user who only ever visits the marketing pages still downloads the checkout flow, the account settings, and the admin dashboard, if none of it is split out.
Splitting the cost across routes and components
Route-based code-splitting ships each route's code as a separate chunk,
loaded when that route is actually visited, instead of upfront. Component-
level splitting goes further, deferring a specific heavy component — a
rich text editor, a chart library, a modal that's rarely opened — until the
moment it's actually needed, via dynamic import().
- Step 1
Initial load
Only the code needed for the current route (and genuinely shared code — layout, framework runtime) downloads and executes.
- Step 2
User navigates to a new route
If that route's chunk isn't already cached or prefetched, a new request fires to fetch it.
- Step 3
Chunk arrives and executes
The new route becomes interactive — this step is where an un-prefetched navigation can introduce a visible delay.
- Step 4
A heavy component is triggered
e.g. opening a rich-text editor modal — its chunk, deferred until now via dynamic import, loads at this point rather than on initial page load.
This isn't a free win — it's a trade. The upfront bundle shrinks, but navigating to a route or triggering a component that wasn't already loaded now costs a network round trip that a single monolithic bundle wouldn't have needed (because everything was already there). The standard mitigation is prefetching: fetching the chunks for routes a user is likely to visit next — hovering a link, or simply everything linked from the current page — ahead of the actual click, so by the time they navigate, the chunk is already sitting in cache.
Setting an actual budget
"Keep the bundle small" isn't a number anyone can act on. A bundle budget — a concrete size threshold, enforced in CI, that fails the build if exceeded — turns this into something automatic and non-negotiable rather than a vague aspiration revisited only when someone notices the app feels slow. A reasonable starting point for an initial-route JS payload, gzipped, is in the range of 150–200KB, though the right number for a given product depends heavily on its actual audience's typical devices and networks — a budget for an internal enterprise tool used on corporate laptops is a different conversation from a budget for a consumer app with meaningful traffic on mid-range phones over patchy connections.
What a budget catches in practice: someone imports an entire utility library for one function instead of the specific export; a duplicate copy of a dependency sneaks in because two packages pinned different versions; a heavy component that should have been dynamically imported gets pulled into the main bundle by an innocent-looking top-level import. None of these individually feel like a big decision at the time they're made — a budget enforced in CI is what turns each one into an immediate, visible failure instead of a slow, invisible regression that only shows up months later as "why does this page feel slower than it used to."
What to remember
- A single bundle for an entire app forces every visitor to pay for code they may never use in that session.
- Route- and component-level code-splitting shrink the upfront cost but introduce later, per-navigation network round-trips — mitigated by prefetching likely-next chunks.
- A concrete, CI-enforced bundle budget turns "bundle size creeps up slowly" into an immediate, actionable failure at the moment a regression is introduced.
- The right budget number depends on the actual audience's devices and networks, not a one-size-fits-all figure.
Check yourself
3 questions · pass 3/3 to unlock Virtualization for Large Lists
1.A single-page app ships one JS bundle containing every route's code, including an admin panel most users never visit. What's the direct cost of this?
2.Route-based code-splitting fixes the 'one giant bundle' problem, but introduces what new cost?
3.A team sets a JS bundle budget of 170KB gzipped for their initial route and enforces it in CI. What's the actual point of this practice?
3 left to answer