Code splitting is the practice of breaking one large JavaScript bundle into several smaller chunks, so the browser downloads only what a given page or feature actually needs instead of the entire application upfront. Without it, every user pays the download cost of every feature on the very first visit, even the admin panel they will never open. With it, the initial bundle shrinks to whatever the first screen requires, and everything else loads on demand.
What changed in 2026
- Framework routers do automatic route-based splitting by default now. Next.js, React Router, and similar tools split each route into its own chunk without manual configuration that used to be a required setup step.
- HTTP/2 and HTTP/3 multiplexing changed the cost math on how granular to split. Loading many small chunks over a multiplexed connection is far cheaper than it was in the HTTP/1.1 era, which shifted best practice toward splitting more freely.
- Bundler-native prefetching became common. Preloading a likely-next chunk on link hover or viewport entry now hides most of the latency a lazy-loaded chunk would otherwise introduce.
- Vendor and app code splitting is more often automatic, separating rarely-changing library code from frequently-changing app code for better long-term browser caching.
How dynamic import creates a split point
// Static import - bundled together, always downloaded upfront
import { renderChart } from "./chart.js";
// Dynamic import - creates a separate chunk, downloaded on demand
button.addEventListener("click", async () => {
const { renderChart } = await import("./chart.js");
renderChart(data);
});
The static import gets bundled into the main chunk no matter what. The dynamic import() call tells the bundler to generate chart.js as a separate file, fetched only when the click handler actually runs. Because import() returns a promise, the same rules from a promise chain apply directly — forgetting to handle a rejected dynamic import fails silently in the same way a forgotten catch does anywhere else.
Route-based splitting in a framework
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./Dashboard.js"));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}
Each route becomes its own chunk automatically. The browser fetches Dashboard.js only when that route actually renders, not when the app first loads.
Code splitting, tree shaking, and lazy loading
These three get conflated constantly. They are related but not the same tool.
| Concept |
What it does |
Removes or defers code? |
| Tree shaking |
Deletes exports that are never used anywhere |
Removes, permanently |
| Code splitting |
Divides used code into multiple chunks |
Defers, does not remove |
| Lazy loading |
The runtime pattern of loading a chunk, image, or module only when needed |
Defers, using code splitting as the mechanism |
See what is lazy loading in 2026 for the runtime pattern that code splitting makes possible, and what is tree shaking in 2026 for the removal side of the equation.
Where to actually split
Split by route or page first — it is the highest value for the least effort. Next, split heavy features that most users never touch, like a rich text editor, a charting library, or PDF export. Below-the-fold sections and modal-only UI are also good candidates. Avoid splitting small, always-used components; the overhead of an extra network request can exceed the savings.
Common mistakes
Splitting too granularly. Many tiny chunks add request overhead that can outweigh the download savings, especially for pieces that load together anyway.
Skipping a loading fallback. A chunk that takes a moment to arrive with no fallback UI reads as a frozen or broken interface rather than a brief load.
Forgetting to preload a chunk you know is coming. The next step of a wizard or the panel a user is about to open is a predictable case worth prefetching ahead of the click.
Accidentally duplicating a shared dependency across chunks. A library imported by two different lazy-loaded features can end up bundled twice unless the bundler is configured to extract it into a shared chunk.
FAQ
Is code splitting only for React?
No. Dynamic import() is a JavaScript language feature, and every major framework and bundler supports splitting on it — Vue, Svelte, Angular, and plain JavaScript with a bundler all use the same underlying mechanism.
Does code splitting slow down the next page a user visits?
It can add a small delay the first time a given chunk is requested, since it was not part of the initial bundle. Prefetching the likely next chunk in advance, such as on link hover, removes most of that delay in practice.
How is code splitting different from lazy loading?
Code splitting is the bundler mechanism that creates separate chunks. Lazy loading is the broader runtime pattern of deferring anything — a chunk, an image, a module — until it is actually needed. Code splitting is what makes JavaScript lazy loading possible.
Do I need a bundler to use dynamic import?
No, import() works natively in modern browsers without a bundler. A bundler adds chunk optimization, shared dependency handling, and prefetching on top of the native behavior.
Where to go next