What is code splitting? How it works and when to use it
Code splitting divides a JavaScript bundle into smaller chunks that load on demand, speeding up initial page load. This guide explains the technique, the tools, and when it matters most.
On this page
- Why bundling alone is not enough
- Route-based and component-based splitting
- Route-based splitting
- Component-based splitting
- React.lazy and Suspense
- When to split and when not to
- Measuring the impact
- FAQ
- What is the difference between code splitting and tree shaking?
- Does code splitting work with server-side rendering?
- What happens if a lazy-loaded chunk fails to load?
- How does code splitting affect caching?
- Summary
Modern web applications ship significant amounts of JavaScript. The median JavaScript payload on mobile devices runs into hundreds of kilobytes, and complex applications can reach several megabytes. Every byte of JavaScript must be downloaded, parsed, compiled, and executed before a page becomes interactive — a process that is measurably slower on mobile devices and constrained networks than on a desktop with a fast connection.
Code splitting is the practice of dividing a JavaScript bundle into smaller, independently loadable pieces so that the browser downloads only the code needed for the current view. The initial page load gets a small bundle with the critical path code; additional code loads only when the user needs it.
Why bundling alone is not enough
Without code splitting, a bundler such as webpack, Rollup, or esbuild produces a single JavaScript file (or a small number of files) containing the entire application. This makes deployment simple and reduces the number of network requests, but it forces users to download the entire application — including code for features they may never use — before the page becomes interactive.
For large single-page applications, this means users on slower connections wait seconds before the page responds. Lighthouse flags a warning when JavaScript execution time exceeds 2 seconds and fails when it exceeds 3.5 seconds. Code splitting directly addresses this by reducing the initial JavaScript payload.
Route-based and component-based splitting
The two main strategies are route-based splitting and component-based splitting.
Route-based splitting
Route-based splitting divides code at the page level. Each route — home, dashboard, settings — becomes its own chunk. The browser downloads the chunk for the current route only; chunks for other routes load only when the user navigates to them.
Most modern frameworks support this automatically. In React, React.lazy() and Suspense handle route splitting. In Vue, dynamic imports on route definitions produce separate chunks. In Svelte, dynamic import() in the router achieves the same result.
Component-based splitting
Component-based splitting targets individual heavy components — a chart library, a rich-text editor, a map widget. These modules may weigh hundreds of kilobytes but are needed only when the user interacts with a specific feature.
The pattern uses the dynamic import() syntax, which returns a promise that resolves once the module loads:
button.addEventListener('click', async () => {
const { Chart } = await import('./chart.js');
new Chart(container, data);
});
The code inside import() does not appear in the initial bundle. The bundler creates a separate chunk that loads only when the event fires.
React.lazy and Suspense
React applications commonly use React.lazy() to split component-level imports. React.lazy() accepts a function that calls import() and returns a component that suspends rendering while the module loads:
import { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import('./HeavyChart'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<HeavyChart />
</Suspense>
);
}
Suspense shows a fallback UI while the chunk downloads. The fallback should be lightweight — a spinner or skeleton — so it renders fast and does not block interactivity.
When to split and when not to
Code splitting adds a network round-trip for each chunk. A page with ten lazy-loaded components triggers ten separate network requests. On HTTP/1.1, this overhead can outweigh the benefit. On HTTP/2 (which most modern servers and CDNs use), multiplexing allows many requests to share a single connection efficiently, making the overhead negligible.
Guidelines:
- Split route-level code. Every significant page or view deserves its own chunk. This is the highest-impact split because the browser downloads nothing it will not need on the current route.
- Defer heavy components. A map, chart, or markdown editor that appears below the fold or behind a click should lazy-load. Do not lazy-load components that are visible above the fold — deferring the hero section delays Largest Contentful Paint (LCP).
- Preload on hover. Adding
<link rel="prefetch">or bundler prefetch hints on navigation links fetches the next route's chunk before the user clicks, making the navigation feel instant.
Measuring the impact
Use the browser's Network panel or Lighthouse to measure JavaScript payload sizes before and after splitting. Webpack Bundle Analyzer and source-map-explorer visualize bundle contents, making it easy to spot large dependencies that belong in a lazy chunk.
Set performance budgets in the bundler configuration to prevent payload sizes from growing silently over time. A budget of 150–200 KB compressed for the initial bundle is a reasonable target for most applications.
FAQ
What is the difference between code splitting and tree shaking?
Tree shaking removes unused code from the bundle at build time based on static import/export analysis. Code splitting divides the bundle into separate files loaded at different times. Both reduce payload size, but tree shaking works at compile time while code splitting works at runtime. They are complementary — use both.
Does code splitting work with server-side rendering?
Yes. Server-side rendered pages still benefit from code splitting on the client. The initial HTML renders on the server, giving users a fast first paint. JavaScript bundles for interactive components then load separately, and hydration happens progressively as chunks arrive. React's Suspense and Next.js dynamic imports work in SSR environments without sacrificing the initial paint.
What happens if a lazy-loaded chunk fails to load?
If the network fails or the server returns an error for a lazy chunk, the dynamic import() promise rejects. Handle this with a .catch() block to show a user-friendly error message and offer a retry option, rather than silently breaking the feature.
How does code splitting affect caching?
Code splitting improves cache efficiency because each chunk changes only when the code it contains changes. A navigation component that never changes produces a chunk that remains cached indefinitely. With a single monolithic bundle, any code change invalidates the entire cache. However, very small chunks (under 5 KB) may be less efficiently cached because the browser overhead per request becomes significant.
Summary
Code splitting is one of the most effective ways to reduce initial page load time. By dividing JavaScript into smaller chunks loaded on demand — route-level or component-level — the browser downloads only the code the user actually needs. Use route-based splitting as a baseline; add component-level splitting for heavy features behind user interaction. Pair splitting with prefetching for navigable routes and measure with Lighthouse to keep payloads within budget.
For more on JavaScript performance, see Web compression: how gzip and Brotli shrink the web and What is a browser cache?. For the broader performance picture, What is caching? covers the full caching stack.
Sources
- MDN — Code splitting (developer.mozilla.org/en-US/docs/Glossary/Code_splitting
- CC-BY-SA 2.5+)
- web.dev — Code-split JavaScript (web.dev/learn/performance/code-split-javascript
- Apache 2.0)
- web.dev — Reduce JavaScript payloads with code splitting (web.dev/articles/codelab-code-splitting
- Apache 2.0)
- MDN — Dynamic import (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
- CC-BY-SA 2.5+)
Share this guide
Related guides
- What is caching? Browser, CDN, server, and database caching explained
How caching works at every layer of the web — browser cache, CDN edge cache, server-side cache, and database cache — and when each one matters for speed and scalability.
- What are Core Web Vitals? LCP, INP, and CLS explained
Core Web Vitals are a set of Google metrics that measure the real-user experience of a web page: loading, interactivity, and visual stability. Learn what LCP, INP, and CLS measure, their target thresholds, and how to improve them.
- What is lazy loading? How deferring resources speeds up the web
Lazy loading is a performance strategy that defers non-critical resources (images, iframes, JavaScript, CSS) until they are needed. Learn what can be lazy-loaded, the native loading attribute, the Intersection Observer API, and when lazy loading helps or hurts.