# Bundle Architecture & Code Splitting
> Ship only the JS a route needs, cache the rest long-term.
Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/bundle-architecture-code-splitting
Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end.
## Summary
Code splitting breaks one large bundle into smaller chunks loaded on demand. Route-based splitting is the minimum viable default: users downloading `/settings` never pay for `/dashboard` code. Component-level splitting with `React.lazy` or `next/dynamic` defers heavy widgets until they are needed. Deterministic chunk IDs make CDN caches survive deploys.
## Code splitting
Without splitting, the browser parses your whole bundle before anything renders — including modules only used on routes the user never visits.
**Code splitting** divides the bundle at defined boundaries so each piece loads on demand. Three levels matter:
1. **Route-based:** the default in Next.js — each page gets its own chunk.
2. **Component-based:** `React.lazy` / `next/dynamic` defers a heavy widget's chunk until that component first mounts.
3. **Vendor chunking:** isolating `node_modules` into a separate chunk lets it cache independently of app code churn.
Modern bundlers (Webpack, Turbopack, Rolldown) handle graph traversal; your job is picking boundaries and verifying output with a bundle analyzer.
## How the bundler produces chunks
- Dynamic `import()` calls are the cut points — each produces a new content-hashed chunk file.
- Deterministic chunk IDs: set `optimization.moduleIds: 'deterministic'` in Webpack (production default); Turbopack uses `turbopackModuleIds: 'deterministic'`.
- Tree-shaking removes dead exports at build time — requires ESM and `"sideEffects": false` in the library's `package.json`.
- Vendor splitting separates `node_modules` from app code so CDN cache survives app-only changes.
- Verify output with `@next/bundle-analyzer` — look for duplicated React, unexpected chunk sizes, and barrel-file bloat.
## Deferring a heavy widget with next/dynamic
Use `next/dynamic` inside a Client Component to defer a heavy widget's chunk. `ssr: false` skips server rendering entirely — it is only valid inside a Client Component marked with `"use client"`.
**Client component wrapper using next/dynamic with ssr: false**
```tsx
// app/dashboard/DashboardClient.tsx — Next.js 16 / React 19
"use client";
import dynamic from "next/dynamic";
// Deferred: HeavyChart is not needed on first paint.
// Its chunk loads only when the component mounts.
const HeavyChart = dynamic(
() => import("@/components/HeavyChart"),
{
loading: () => ,
ssr: false, // ssr: false is only valid inside Client Components
}
);
export default function DashboardClient() {
return (
{/* in the route chunk — always needed */}
{/* in its own chunk — deferred */}
);
}
```
`ssr: false` is only valid in Client Components. The `"use client"` directive makes this file a Client Component; the Server Component page imports it.
## Diagram
```mermaid
graph TD
A[User navigates to /dashboard] --> B[Route chunk: dashboard.js]
B --> C{HeavyChart mounts?}
C -- yes --> D[Fetch: heavy-chart-a3f2.js]
C -- no --> E[Skip — chunk not loaded]
B --> F[vendor-react-b1c2.js]
F --> G[(CDN cache — stable hash)]
D --> H[Render chart]
```
Route chunk loads immediately; the HeavyChart chunk fetches only on mount; the vendor chunk is served from CDN cache because its hash is stable.
## Watch out: Over-splitting backfires
Too many small chunks cause waterfall requests that exceed HTTP/2 multiplexing gains. Splitting a component under ~5 kB gzipped usually costs more in round-trips than it saves in parse time. Never split a component needed on first paint above the fold.
## Tradeoffs
**Pros**
- Initial parse time drops when only the current-route chunk loads.
- Vendor chunks cache across deploys if their content hash is stable.
- Large, rarely-used widgets load only when rendered — no upfront cost.
- Smaller chunks can load in parallel over HTTP/2, reducing total time.
**Cons**
- Too many small chunks cause waterfall requests that exceed HTTP/2 multiplexing gains.
- Dynamic imports add a network round-trip on first render, causing visible loading states.
- Misconfigured splits can duplicate React itself across chunks.
- Tree-shaking fails silently on CommonJS imports, so chunk sizes mislead without analysis.
## Key terms
- **Dynamic import**: A native `import()` call that tells the bundler to cut a new chunk loaded on demand.
- **Tree-shaking**: Dead-code elimination at build time; works on ESM static imports, not CommonJS `require`.
- **Deterministic chunk ID**: A hash derived from module content/path so a chunk's filename is stable across deploys.
- **SplitChunksPlugin**: Webpack plugin that extracts shared modules into separate chunks based on size and reuse.
- **Vendor chunk**: A chunk containing only `node_modules` code, cached independently from fast-changing app code.
## Related topics
- [Caching Strategies](https://fearchitect.com/topics/caching-strategies.md): Layer browser, CDN, and app caches to serve responses without re-fetching.
- [Render Performance](https://fearchitect.com/topics/render-performance-patterns.md): Skip renders, defer slow work, and virtualize long lists.
- [Microfrontends & Module Federation](https://fearchitect.com/topics/microfrontends-module-federation.md): Independently deployable frontends composed at runtime in the browser.
- [Monorepos (Turborepo / Nx)](https://fearchitect.com/topics/monorepos.md): One git repo, many packages, shared tooling and task caching.
- [CI/CD for Frontend](https://fearchitect.com/topics/ci-cd-frontend.md): Automated pipeline from commit to production with quality gates.
- [CSS Modules vs CSS-in-JS vs Tailwind](https://fearchitect.com/topics/css-modules-vs-css-in-js-vs-tailwind.md): CSS Modules, CSS-in-JS, and utility-first — by runtime cost and RSC fit.
## Further reading
- [Next.js — Optimizing: Bundle Analyzer](https://nextjs.org/docs/app/guides/package-bundling)
- [web.dev — Reduce JavaScript payloads with code splitting](https://web.dev/articles/reduce-javascript-payloads-with-code-splitting)
- [Next.js — Lazy loading](https://nextjs.org/docs/app/guides/lazy-loading)
- [Webpack — SplitChunksPlugin](https://webpack.js.org/plugins/split-chunks-plugin/)