Code splitting: ship the JavaScript this page needs, not all of it
A single bundle has one fatal property: to see the login page, the user downloads the admin dashboard, the charting library, the rich-text editor, and every route they will never visit. The browser must parse and compile all of it before the app is interactive, so the cost is not only bytes on the wire — it is main-thread time. Code splitting breaks that one file into pieces the app loads on demand: the first screen ships only its own code, and the rest arrives when the user actually navigates to it.
The dynamic import is the split point
Bundlers split at one syntactic marker: the dynamic import(). Where a static
import pulls code into the current chunk, import() returns a promise and tells
the bundler “put this in its own file and fetch it at runtime.” In a framework
this is wrapped in a lazy-loading helper so a whole route becomes a chunk:
import { lazy, Suspense } from "react";
// Dashboard and its deps become a SEPARATE chunk, fetched on first render
const Dashboard = lazy(() => import("./routes/Dashboard.jsx"));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard /> {/* the chunk downloads when this mounts */}
</Suspense>
);
}
The Suspense fallback is not optional polish — it is what the user sees during
the network round-trip for the chunk, so a lazy boundary without a good fallback
just trades a slow start for a blank flash.
Prefetch so the split is invisible
Splitting adds a delay at the moment of navigation: the chunk has to arrive before the route can render. You hide that delay by fetching the chunk before the click, during idle time or on hover — the code is ready by the time the user commits:
// warm the dashboard chunk when the link is hovered, before the click
link.addEventListener("mouseenter", () => {
import("./routes/Dashboard.jsx"); // browser caches it; navigation is instant
}, { once: true });
Split by route first, then by weight
Not every import() earns its round-trip. The wins come from two places: split
by route, because a user on the login page genuinely does not need the
dashboard; and split out heavy, rarely-used dependencies — a charting library,
a markdown editor, a date picker — that would otherwise inflate the entry chunk
for everyone. Over-splitting has its own cost: dozens of tiny chunks mean dozens
of requests and worse compression, so there is a floor below which a chunk is not
worth its own file. The discipline is to set a bundle budget and let it fail the
build when the entry chunk crosses it, which is exactly what the bundle-budget
exercise makes you do — a number that turns “ship less JavaScript” from a wish
into a check.