# 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/)