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.
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.
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:
import _ from 'lodash' — use import { debounce } from 'lodash-es' instead (lodash-es uses ES modules)"sideEffects": false in package.jsonrequire() is not statically analyzable. Many older packages must be explicitly configured for tree shaking.Instead of shipping one huge JS file, code splitting produces multiple smaller chunks. The browser only downloads what it needs for the current page.
// 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>
);
}
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'],
}
}
}
}
// 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.
You can't optimize what you can't measure. Tools:
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.
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
What is tree shaking and what conditions must be met for it to work?
What is code splitting and how does dynamic import() enable it?
How do you analyze and diagnose large bundle sizes?
What is vendor chunk splitting and why is it important for caching?
What is the module/nomodule pattern for differential serving?
What is lazy loading and how does it apply to images and components?
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.