Code Splitting
Date: 2026-08-16
Break the bundle so each page downloads only what it needs. The gain isn’t the bytes saved on the network — it’s the parse, compile and execute time you never spend on code that page was never going to run.
What it is
Code splitting is dividing an application bundle into chunks loaded on demand, rather than shipping everything on first load.
SINGLE BUNDLE SPLIT BY ROUTE
app.js 820 KB shared.js 180 KB every page
every page pays for home.js 40 KB homepage only
the checkout code, category.js 90 KB category only
the account code, product.js 110 KB product only
the admin code checkout.js 240 KB checkout only
product page pays 820 KB product page pays 290 KB
The saving compounds: those 530KB were never downloaded, never parsed, never compiled and never executed — see JavaScript Execution Cost.
The two axes
By route. The default and the biggest win. Each page loads its own chunk plus a shared core. Every meta-framework does this automatically from the file structure.
By interaction. Load a component’s code when it’s needed rather than when the page loads:
// loaded on page load — costs everyone
import SizeGuideModal from './SizeGuideModal';
// loaded when someone opens it — costs the people who use it
const openSizeGuide = async () => {
const { default: SizeGuideModal } = await import('./SizeGuideModal');
render(SizeGuideModal);
};Good candidates: modals, video players, maps, rich text editors, date pickers, anything below the fold, anything behind a tab.
Where it goes wrong
Over-splitting. Every chunk is a request with its own round-trip latency. Fifty tiny chunks on a mobile connection is slower than five sensible ones, and it defeats compression, which works better across larger files.
Rough guide: chunks in the tens of kilobytes, not the single digits.
Waterfalls. A chunk that imports another chunk that imports a third produces sequential round trips:
BAD BETTER
page.js page.js
→ widget.js → widget.js ┐ requested in parallel
→ helper.js → helper.js ┘
3 sequential round trips 2 parallel
Preloading the next chunk while the current one runs is the usual mitigation — most bundlers emit modulepreload hints for this.
Splitting shared code badly. If a utility is used by four routes and ends up duplicated in four chunks, you’ve made things worse. Bundlers handle this with a shared or common chunk; check the output rather than assuming.
Splitting the critical path. Code needed for first render should be in the initial chunk. Deferring it adds a round trip before the page can paint — The Critical Rendering Path.
Verifying it worked
Bundle analysis is not optional here, because the output rarely matches the mental model:
1 generate a bundle report (webpack-bundle-analyzer, rollup-plugin-visualizer,
or the framework's built-in build output)
2 look for: duplicated dependencies across chunks
a single dependency dominating a chunk
anything large in the shared chunk that only one route uses
3 DevTools → Coverage: what proportion of the loaded JS actually ran
Duplicated dependencies are the most common finding — two versions of the same library because two packages depend on different ranges. Deduplicating often saves more than any splitting decision.
Prefetching the next step
Splitting means the next page’s chunk isn’t loaded yet. For a known funnel this is worth pre-empting:
<link rel="prefetch" href="/assets/checkout.a4f2.js">Lowest priority, fetched when idle, so the next navigation is instant. Sensible for a strongly-directional flow — basket to checkout — and wasteful when applied everywhere, because it spends the customer’s data on pages they won’t visit. See Resource Hints.
What it doesn’t fix
- Total code volume. Splitting redistributes; it doesn’t reduce. If checkout genuinely needs 240KB, checkout is still slow
- Third-party scripts. They’re loaded outside your bundle and unaffected — Third-Party Scripts
- Hydration cost. Splitting reduces what’s downloaded, not necessarily what’s hydrated — Hydration, Islands Architecture