Advanced6 questionsFull Guide

Bundle Optimization (Tree Shaking, Code Splitting) — System Design Interview Guide

Deep-dive into tree shaking, code splitting, dynamic imports, chunk strategy, and module analysis. Understand how to reduce JS bundle size, what tools to use, and how to answer every bundle optimization question with concrete techniques and numbers.

The Mental Model

Think of your JavaScript bundle like a grocery shipment. Without optimization, you send the entire warehouse to every customer — even if they only ordered milk. Tree shaking removes the products nobody ordered (dead code). Code splitting breaks the warehouse into departments — each department is shipped only when the customer walks into that aisle. Dynamic imports are the 'deliver on demand' option — you only request the department when the customer actually needs it.

The Explanation

Tree Shaking — Eliminating Dead Code

Tree shaking is the build-time elimination of exported-but-never-imported code. It relies on ES module static analysis — import/export statements are analyzable at build time; require() is not.

// math.js — only add is used by the app
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; } // never imported

// main.js
import { add } from './math';    // only add is included in the bundle
// multiply is tree-shaken out — never shipped to the browser

Common mistakes that break tree shaking:

  • Importing the entire library: import _ from 'lodash' — use import { debounce } from 'lodash-es' instead (lodash-es uses ES modules)
  • Side-effect imports: files that run code on import cannot be shaken. Mark your package as side-effect-free: "sideEffects": false in package.json
  • CommonJS dependencies — CJS require() is not statically analyzable. Many older packages must be explicitly configured for tree shaking.

Code Splitting — Splitting One Bundle Into Many

Instead of shipping one huge JS file, code splitting produces multiple smaller chunks. The browser only downloads what it needs for the current page.

Route-Based Splitting (Most Impactful)

// React Router + dynamic import — each route is a separate chunk
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Vendor Splitting

Separate your code from third-party libraries. Your code changes on every deploy; React, lodash, etc. don't. Split them so users keep the vendor chunk cached while only redownloading your application code.

// Vite — manual chunks
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        vendor: ['react', 'react-dom'],
        charts: ['recharts'],
      }
    }
  }
}

Dynamic Imports — Load on Demand

// Load a heavy library only when the user opens the modal
async function openChartModal() {
  const { Chart } = await import('chart.js'); // downloaded now, not at page load
  renderChart(Chart);
}

Dynamic imports are ideal for: rich text editors, charting libraries, PDF generators, anything large that isn't needed on initial load.

Analyzing Your Bundle

You can't optimize what you can't measure. Tools:

  • webpack-bundle-analyzer — interactive treemap of what's in your bundle
  • vite-bundle-visualizer — similar for Vite projects
  • source-map-explorer — works on any build with source maps
  • bundlephobia.com — check npm package size before installing

Common Misconceptions

⚠️

Tree shaking removes all unused code — it only removes unused *exports*. Code that has side effects on import (modifying globals, etc.) cannot be safely removed even if it's never called.

⚠️

Webpack automatically tree-shakes everything — only works with ES modules (import/export). If a dependency ships CommonJS, you need special plugins or to find an ESM-compatible alternative.

⚠️

More code splitting is always better — too many tiny chunks means more HTTP requests and more overhead. The optimal chunk size is typically 50–200KB. Below 20KB chunks add more overhead than they save.

⚠️

Dynamic imports are the same as lazy loading — React.lazy is a React-specific wrapper around dynamic import. The import() syntax itself is a JavaScript language feature.

Where You'll See This in Real Code

Lodash: switching from `import _ from 'lodash'` to `import { debounce } from 'lodash-es'` can reduce bundle by 50-70KB

Next.js: automatic code splitting per page — visiting /home only downloads the home page chunk, not the entire app

Rich text editors (TipTap, Quill): loaded dynamically only when user focuses a text area, not on initial page load

Stripe.js: loaded via dynamic import only on payment pages — unnecessary on most pages of a site

Interview Cheat Sheet

  • Tree shaking: dead export elimination; requires ES modules (import/export not require)
  • sideEffects: false in package.json — tells bundler the package has no side-effect imports
  • Code splitting: one bundle → many chunks; route-based splitting is highest impact
  • React.lazy() + Suspense: lazy-load React components with automatic code splitting
  • dynamic import(): load any module on demand — works without React
  • Vendor splitting: separate node_modules from app code for better long-term caching
  • Target chunk size: 50–200KB per chunk; below 20KB adds more overhead than it saves
  • Analysis tools: webpack-bundle-analyzer, vite-bundle-visualizer, bundlephobia
💡

How to Answer in an Interview

  • 1.Start with measurement — mention bundle-analyzer before talking about solutions. 'I'd first visualize the bundle to find the biggest wins' shows senior thinking.
  • 2.Route-based code splitting is almost always the highest-impact optimization — mention it first, then discuss granular dynamic imports
  • 3.Know the three conditions for tree shaking: ES modules, no side effects, and bundler optimization mode (production build)
  • 4.Vendor chunk splitting and long-term caching is a follow-up many miss — separating vendor code lets browsers cache React across deploys

Practice Questions

6 questions
#01

What is tree shaking and what conditions must be met for it to work?

EasyBundle Optimization PRO💡 Dead code elimination via static analysis of ES module imports — requires ESM, no side-effect imports, no CommonJS
#02

What is code splitting and how does dynamic import() enable it?

EasyBundle Optimization PRO💡 Split the bundle into chunks loaded on demand — dynamic import() is the mechanism, React.lazy is the React wrapper
#03

How do you analyze and diagnose large bundle sizes?

EasyBundle Optimization PRO💡 webpack-bundle-analyzer, Vite's rollup-plugin-visualizer, source-map-explorer — find what is large and why
#04

What is vendor chunk splitting and why is it important for caching?

EasyBundle Optimization PRO💡 Separate third-party code (React, lodash) from app code — vendors change rarely so their chunk stays cached across deploys
#05

What is the module/nomodule pattern for differential serving?

MediumBundle Optimization PRO💡 Ship modern ESM to capable browsers and legacy transpiled bundle to IE/old browsers — reduces bundle size for majority of users
#06

What is lazy loading and how does it apply to images and components?

EasyBundle Optimization PRO💡 Defer loading of off-screen resources until needed — native loading="lazy" for images, React.lazy + Suspense for components

Related Topics

Core Web Vitals & Performance — System Design Interview Guide
Senior·8–12 Qs
Network Optimization (Prefetch, Preload, HTTP/2) — System Design Interview Guide
Advanced·8–10 Qs
Rendering Strategies (SSR, CSR, SSG, ISR) — System Design Interview Guide
Senior·10–15 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz