JavaScript Interview Prep
Performance & Optimization

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: false flag in package.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


Tree Shaking & Dead Code Elimination thumbnail


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: false in package.json works 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

Tree Shaking & Dead Code Elimination visual 1


Common Mistakes

  • Importing a library with import _ from "lodash" — pulls in the entire 500KB+ CommonJS build. Use lodash-es with 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 — import and export statements 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/export are static declarations — always at the top level, never conditional — enabling static analysis.

Q: What does the sideEffects field in package.json do?

The sideEffects field 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": false means 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 .add you 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.

  1. CommonJS require() — dynamic, can't be statically analyzed. 2) Module-level side effects (top-level console.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 with export * 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.

On this page