Tree Shaking & Dead Code Elimination
Ship Only What You Use
LinkedIn Hook
You imported one function from lodash. Your bundle gained 531KB.
That's tree shaking failing, silently. The bundler couldn't prove what was safe to remove, so it kept everything. Your user paid for 530KB of code they never touched.
Tree shaking works only when the bundler can statically analyze your imports — which means ES Modules, no side effects, named exports, and no
import *. Break any of those rules and dead code sticks.In this lesson: how ES module static analysis actually enables tree shaking, the 5 patterns that silently break it (CommonJS, side effects, wildcard imports, dynamic paths, barrel files), the magic
sideEffects: falseflag inpackage.json, and how to spot the damage with webpack-bundle-analyzer or bundlephobia.com.If your production bundle is bigger than it should be — this lesson is the fix.
Read the full lesson -> [link]
#JavaScript #TreeShaking #BundleSize #WebPerformance #Webpack #Vite #Frontend #InterviewPrep
What You'll Learn
- Why ES Modules can be tree-shaken but CommonJS cannot — the role of static analysis
- The five patterns that silently break tree shaking and how to write tree-shakeable code
- How
sideEffects: falseinpackage.jsonworks and how to audit your bundle size
The Apple Tree Analogy
Think of tree shaking like harvesting an apple tree. You only pick the ripe apples (used exports) and leave the rest. If a branch has no ripe apples at all, you prune the entire branch. The tree itself is your module, branches are exported functions, and the "shake" is the bundler removing everything your app never imports.
How ES Module Static Analysis Enables Tree Shaking
// ES Modules — static structure, can be analyzed at build time
import { add } from "./math.js";
// The bundler KNOWS at build time:
// - Only "add" is used from math.js
// - "subtract", "multiply", "divide" can be removed
// CommonJS — dynamic, CANNOT be tree-shaken
const math = require("./math.js"); // what's used? unknown until runtime
const { add } = require("./math.js"); // still dynamic — require runs at runtime
What Gets Tree-Shaken vs What Doesn't
// math.js — a tree-shakeable module
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
// main.js — only imports add
import { add } from "./math.js";
console.log(add(2, 3));
// After tree shaking, the bundle only contains:
// function add(a, b) { return a + b; }
// console.log(add(2, 3));
// subtract, multiply, divide are REMOVED
What Breaks Tree Shaking
// BREAK 1: CommonJS modules
const _ = require("lodash"); // entire lodash included (500KB+)
_.get(obj, "a.b.c");
// FIX: Use ES module version or specific imports
import get from "lodash-es/get"; // only 'get' is bundled
// BREAK 2: Side effects at module level
// utils.js
export function helper() { /* ... */ }
console.log("Module loaded!"); // SIDE EFFECT — can't remove this module
document.title = "Changed!"; // SIDE EFFECT
Array.prototype.customMethod = function() {}; // SIDE EFFECT
// Even if helper() is never imported, the side effects
// force the bundler to include this entire module
// BREAK 3: Namespace / wildcard imports
import * as utils from "./utils.js"; // all exports kept
utils.something(); // bundler can't be sure what's used dynamically
// FIX: Named imports only
import { something } from "./utils.js";
// BREAK 4: Dynamic import with variable path
const module = await import(`./pages/${pageName}.js`);
// Bundler can't statically determine which file — includes ALL matches
// BREAK 5: Re-exporting everything
// index.js (barrel file)
export * from "./moduleA.js";
export * from "./moduleB.js";
export * from "./moduleC.js";
// Some bundlers struggle with barrel files — check your output!
sideEffects Flag in package.json
{
"name": "my-library",
"version": "1.0.0",
"sideEffects": false
}
// sideEffects: false tells the bundler:
// "Every file in this package is pure — safe to tree-shake"
// If unused exports are found, the entire module can be removed
// For specific files that DO have side effects:
// "sideEffects": ["*.css", "./src/polyfills.js"]
// This means: tree-shake everything EXCEPT CSS files and polyfills.js
Checking Bundle Size
// Tool 1: webpack-bundle-analyzer
// npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin() // opens interactive treemap
]
};
// Tool 2: source-map-explorer (works with any bundler)
// npx source-map-explorer dist/main.js
// Tool 3: bundlephobia.com — check package size BEFORE installing
// https://bundlephobia.com/package/lodash -> 531KB
// https://bundlephobia.com/package/lodash-es -> 531KB but tree-shakeable
// https://bundlephobia.com/package/date-fns -> tree-shakeable
// https://bundlephobia.com/package/moment -> NOT tree-shakeable (338KB)
Practical Tips for Tree-Shakeable Code
// TIP 1: Write pure functions — no side effects
// BAD
let counter = 0;
export function increment() {
counter++; // mutates outer scope — side effect
return counter;
}
// GOOD
export function increment(counter) {
return counter + 1; // pure — no side effects
}
// TIP 2: Use named exports, not default exports
// Default exports are harder for bundlers to tree-shake
// BAD — default export of object
export default {
add(a, b) { return a + b; },
subtract(a, b) { return a - b; }
};
// Importing ONE method pulls in the ENTIRE object
// GOOD — named exports
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// TIP 3: Avoid classes with many methods if only one is used
// Classes can't be partially tree-shaken
// BAD — entire class included even if you use one method
export class MathUtils {
static add(a, b) { return a + b; }
static subtract(a, b) { return a - b; }
static multiply(a, b) { return a * b; }
// 50 more methods...
}
// Using MathUtils.add imports ALL methods
// GOOD — individual functions
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// TIP 4: Import specific methods from large libraries
// BAD
import moment from "moment"; // 338KB all included
// GOOD
import { format, parseISO } from "date-fns"; // only what you use
Common Mistakes
- Importing a library with
import _ from "lodash"— pulls in the entire 500KB+ CommonJS build. Uselodash-eswith named imports (import { get } from "lodash-es") or the per-method packages (import get from "lodash/get"). - Writing top-level code in a module (
console.log, polyfills,Array.prototype.x = ...) and expecting the bundler to drop it — those are side effects, so the entire module sticks around even if nothing from it is imported. - Using
export default { add, subtract, ... }— a default-exported object is opaque to tree shaking. Switch to named exports so the bundler can drop each function independently.
Interview Questions
Q: What is tree shaking and how does it work?
Tree shaking is a dead code elimination technique that removes unused exports from the final bundle. It works because ES Modules have a static structure —
importandexportstatements are analyzed at build time (not runtime), so the bundler can determine which exports are actually used and safely remove the rest. The term comes from the idea of "shaking" a dependency tree and letting dead code fall off.
Q: Why can't CommonJS modules be tree-shaken?
CommonJS
require()is a regular function call that executes at runtime. It can be called conditionally (if (x) require("y")), with variables (require(path)), or anywhere in the code. The bundler can't statically determine at build time what's being imported or used, so it must include everything. ES Modules'import/exportare static declarations — always at the top level, never conditional — enabling static analysis.
Q: What does the sideEffects field in package.json do?
The
sideEffectsfield tells the bundler whether a package's modules have side effects (code that runs just by being imported, like polyfills, CSS imports, or global mutations). Setting"sideEffects": falsemeans all modules are pure — if their exports aren't used, the entire module can be safely removed. You can also specify an array of files that DO have side effects:"sideEffects": ["*.css", "./polyfills.js"].
Q: Why are named exports more tree-shakeable than default exports?
Named exports (
export function add() {}) expose each binding as an individually named top-level symbol, so the bundler can keep or drop each one. A default export of an object (export default { add, subtract }) is a single value — to use.addyou must import the whole object, which pins every property in place. Named exports give the bundler finer-grained granularity.
Q: What breaks tree shaking? Name 4 things.
- CommonJS
require()— dynamic, can't be statically analyzed. 2) Module-level side effects (top-levelconsole.log, prototype mutations, polyfill registration). 3) Namespace imports (import * as X from "./y") — the bundler has to assume anything could be used. 4) Dynamic import paths with template literals (import('./p/' + name)) — the bundler includes every file that might match. Bonus: barrel files withexport * from ...sometimes confuse bundlers.
Quick Reference — Cheat Sheet
TREE SHAKING — SHIP ONLY WHAT'S USED
REQUIRES
ES Modules (import/export, static)
Minification step (Terser / esbuild)
A bundler that supports it (webpack, Rollup, Vite, esbuild)
BREAKS IT
CommonJS require()
module-level side effects (console.log, polyfills, proto mutation)
import * as X from "..."
dynamic import with template-literal path
export default { a, b, c } (opaque object)
HELPS IT
named exports only
pure functions, no side effects
import { get } from "lodash-es" NOT import _ from "lodash"
package.json -> "sideEffects": false
(or array of files that DO have side effects)
AUDIT TOOLS
webpack-bundle-analyzer -> interactive treemap
source-map-explorer -> works with any bundler
bundlephobia.com -> size check before npm install
QUICK WINS
date-fns over moment (tree-shakeable)
lodash-es over lodash (named ESM)
avoid giant barrel files in hot paths
Previous: requestIdleCallback -> Non-Critical Work Without Blocking Frames Next: setTimeout vs setInterval -> Scheduling Delayed Work
This is Lesson 13.5 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.