What is Code Splitting?
Code splitting is a performance technique that breaks JavaScript bundles into smaller chunks loaded on demand.
What is Code Splitting?
Code splitting is a front-end performance technique that breaks a single large JavaScript bundle into smaller chunks, loading each chunk only when it's needed. The concept has existed since webpack 1 (2014), but it became a first-class pattern with React.lazy() in React 16.6 (October 2018) and ES2020's dynamic import() syntax. By deferring non-critical code, code splitting directly reduces the amount of JavaScript parsed and executed during initial page load. This shrinks Time to Interactive, improves Largest Contentful Paint (LCP), and lowers Interaction to Next Paint (INP) because the main thread isn't blocked evaluating unused code. A typical SPA shipping 400KB of JavaScript can often cut its initial payload to 80-120KB with route-based splitting alone. We use code splitting on virtually every Next.js and Astro project we ship — it's the single highest-ROI performance optimization for JavaScript-heavy sites.
How it works
Code splitting relies on the bundler's ability to identify split points — places in your code where a chunk boundary should exist — and produce separate files for each.
Static analysis (route-based splitting): Frameworks like Next.js do this automatically. Each page in app/ or pages/ becomes its own chunk. When a user navigates to /dashboard, only the dashboard chunk loads.
Dynamic imports (component-level splitting): You explicitly mark a component or module as lazy-loaded:
// Next.js / React pattern
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <p>Loading chart…</p>,
ssr: false,
});
At build time, the bundler (webpack, Turbopack, Vite/Rollup) extracts HeavyChart and its dependencies into a separate .js file. At runtime, that file is fetched only when <HeavyChart /> renders.
Shared chunks: Bundlers also deduplicate. If two routes both import lodash-es, the bundler extracts it into a shared chunk so it's cached once and reused.
Prefetching: Next.js prefetches linked route chunks on viewport intersection by default (the <Link> component). Astro takes a different approach — it ships zero JS by default and only hydrates interactive islands, which is code splitting taken to its logical extreme.
The key metric to watch is your initial JS payload. Run next build and check the "First Load JS" column. Anything above 150KB (gzipped) for a route deserves investigation.
When to use it
Code splitting is almost always a good idea. Here's when it matters most — and when it doesn't.
Use it when:
- Your initial bundle exceeds 100KB gzipped
- You have heavy third-party libraries (charting, rich text editors, maps) that aren't needed on first render
- You're building an SPA or hybrid app with multiple routes
- You see poor LCP or INP scores and the main thread is blocked by JavaScript evaluation (check Chrome DevTools → Performance panel)
- You ship admin-only or authenticated-only features alongside your public site
Skip it (or don't over-optimize) when:
- You're building a static/content site with minimal JS (Astro's island architecture already handles this)
- Your total JS is under 50KB gzipped — the overhead of extra network requests can negate the benefit
- You're splitting so aggressively that you create waterfall chains (chunk A loads chunk B loads chunk C)
Code Splitting vs alternatives
| Technique | What it does | When to pick it |
|---|---|---|
| Code splitting | Breaks JS into on-demand chunks | SPAs, hybrid apps, any JS-heavy page |
| Tree shaking | Removes unused exports at build time | Always — it's complementary, not a replacement |
| Lazy hydration | Defers hydrating server-rendered components | Astro islands, React Server Components |
| Bundle size reduction | Swap heavy deps for lighter ones (e.g., date-fns over moment) | Before splitting — reduce first, split second |
| Compression (gzip/Brotli) | Shrinks bytes over the wire | Always on, but doesn't reduce parse/eval time |
Code splitting and tree shaking work together. Tree shaking removes dead code from each chunk; code splitting controls when chunks load. We've shipped projects where tree shaking alone cut 30% of bundle weight, but code splitting was still needed to defer the remaining 200KB of charting code until the user actually opened a dashboard tab.
Real-world example
On a Next.js 14 e-commerce build, the product detail page shipped 380KB of initial JS (gzipped). The culprits: a reviews widget using a rich text editor (Tiptap, ~90KB), a 3D product viewer (Three.js, ~140KB), and analytics SDKs.
We applied three changes:
next/dynamicwithssr: falsefor the 3D viewer — loaded only when the user clicked "View in 3D"- Dynamic import for Tiptap, triggered when the review form opened
- Moved analytics to a web worker via Partytown
Initial JS dropped to 112KB gzipped. LCP improved from 3.8s to 2.1s on mobile 4G. INP went from 340ms to 180ms — comfortably under the 200ms "good" threshold. The entire effort took one engineer about six hours.