How do code splitting, lazy loading, and tree shaking reduce JS weight?
Take a moment.
Say your answer out loud or write it down first — retrieval is what makes it stick. Reveal when you're ready.
Short answer
Code splitting breaks one giant bundle into route- or component-level chunks loaded on demand; lazy loading (dynamic import, React.lazy) defers non-critical chunks until needed; tree shaking statically drops unused exports at build time. Together they shrink the initial JS parsed, compiled, and executed before interactivity.
Detailed explanation
JavaScript cost isn't just bytes — the browser must download, parse, compile, and execute every byte on the main thread, and mobile CPUs do this 5–10x slower than your laptop. A 2MB bundle can mean seconds of locked-up page on a mid-range phone. Code splitting attacks the shape: bundlers (Vite/Rollup/webpack) emit separate chunks per route or per dynamic import(), so the homepage ships 80KB while the admin dashboard's 500KB loads only when visited. Route-level splitting is the default win; component-level (modals, editors, charts below the fold) goes further. Lazy loading is the loading policy on top: React.lazy + Suspense, Vue's defineAsyncComponent, or raw import('./editor.js') on user intent (hover, click, IntersectionObserver) — prefetch on idle or hover to hide the latency. Tree shaking attacks dead code: ESM's static imports let Rollup prove an export is never used and drop it (works only with side-effect-free modules — mark "sideEffects": false and avoid barrel files that re-export everything). Complement with: analyzing (rollup-plugin-visualizer/bundle analyzer to find the whales — usually moment, lodash-full, or duplicate deps), replacing heavy libs (date-fns over moment), and serving modern syntax to modern browsers (fewer transpiled helpers). Measure the before/after on total blocking time, not just kilobytes.
Example
// Route-level splitting (React Router)
// const Dashboard = lazy(() => import('./routes/Dashboard.jsx'));
// <Suspense fallback={<Spinner />}><Dashboard /></Suspense>
// Load a heavy editor only when the user asks
async function openEditor() {
const { createEditor } = await import('./editor.js'); // separate chunk
createEditor(document.getElementById('pad'));
}
// Prefetch on idle so it's instant when clicked
// <link rel="prefetch" href="/assets/editor.chunk.js" as="script" />
// package.json — unlock full tree shaking
// { "sideEffects": false }Interview tip
Quantify the enemy (parse/compile on weak phones, not just bytes), then stack the three techniques in build-order: shake dead code, split the rest, lazy-load by intent. Naming the bundle analyzer shows workflow maturity.
Common mistakes
- Splitting into hundreds of tiny chunks, trading parse cost for request waterfalls.
- Lazy-loading above-the-fold content, delaying LCP.
- Barrel files and side-effectful modules silently defeating tree shaking.