Skip to content
Now accepting Q2 projects — limited slots available. Get started →
Performance · Updated Apr 30, 2026

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:

  1. next/dynamic with ssr: false for the 3D viewer — loaded only when the user clicked "View in 3D"
  2. Dynamic import for Tiptap, triggered when the review form opened
  3. 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.

Frequently asked questions about Code Splitting

Is code splitting the same as tree shaking?
No. Tree shaking removes unused exports from your code at build time — it makes each module smaller. Code splitting takes the remaining modules and breaks them into separate files loaded on demand. They're complementary. Tree shaking is about removing dead code; code splitting is about controlling *when* live code loads. Most modern bundlers (webpack 5, Rollup, Turbopack) do both, but they solve different problems. You should always have tree shaking enabled (it's on by default in production builds) and then apply code splitting on top.
When did code splitting become standard practice?
Webpack introduced code splitting support in version 1 (2014), but it required the non-standard `require.ensure` syntax and wasn't widely adopted. The real inflection point was the TC39 dynamic `import()` proposal reaching Stage 3 in 2017 and webpack 4 shipping `splitChunks` optimization in February 2018. React 16.6 added `React.lazy()` in October 2018, making component-level splitting trivial. By 2020, Next.js had automatic route-based splitting and most teams considered it table stakes. Today in 2026, if your framework doesn't split by default, something's wrong.
What's the alternative to code splitting?
The main alternatives are shipping less JavaScript in the first place or using a framework architecture that avoids large client bundles. Astro's island architecture is a great example — it sends zero JS by default and only hydrates interactive components. React Server Components (stable in Next.js 13.4+) keep component logic on the server so it never hits the client bundle at all. For simpler sites, plain HTML with progressive enhancement can eliminate the need for splitting entirely. But for any app with meaningful client-side interactivity, code splitting remains the primary tool.
Does Next.js do code splitting automatically?
Yes. Next.js has automatic route-based code splitting built in since its earliest versions. Every file in `app/` or `pages/` becomes its own chunk. Shared dependencies are extracted into common chunks automatically. However, automatic splitting only covers route boundaries. If a single route imports a 200KB charting library that only 10% of users interact with, you still need manual component-level splitting using `next/dynamic` or `React.lazy()`. We treat automatic route splitting as the baseline and then profile each route's "First Load JS" from `next build` output to find components worth splitting further.
Get in touch

Let's build
something together.

Whether it's a migration, a new build, or an SEO challenge — the Social Animal team would love to hear from you.

Get in touch →