post's image

Daily Depth #4: Understanding Tree Shaking

Ghost wrote 2 days ago (May 17, 2025) with 63 | 4 mins read

Tree Shaking in depth

Tree shaking is the process of removing unused code (“dead code elimination”) from JavaScript bundles by leveraging the static import/export structure of ES modules. In practice, bundlers parse your code into an Abstract Syntax Tree (AST), follow import/export links, and mark which exports are actually used. Unreferenced functions or variables can then be omitted from the final bundle.

It’s a crucial optimization for frontend performance, especially for applications depending on large libraries like Lodash, D3, or Moment.js.


🔍 How Tree Shaking Works

Tree shaking relies on the ability to statically analyze code. This is why it works well with:

  • ES Modules (ESM): Static import/export syntax is required.
  • Bundlers: Tools like Webpack, Rollup, esbuild, and Vite use ASTs to determine which exports are unused.

✅ Shakeable Code Example

// math.js
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// app.js
import { add } from './math.js';

console.log(add(2, 3)); // Only `add` should be kept in the bundle

If your bundler is configured correctly, the multiply function will be removed from the final build.

❌ Unshakeable Code Example

// utils.js
export const utils = {
  isString(val) {
    return typeof val === 'string';
  },
  isNumber(val) {
    return typeof val === 'number';
  }
};

// app.js
import { utils } from './utils.js';
console.log(utils.isString('hello'));

In this case, both isString and isNumber are retained because utils is an object that references both. The bundler can’t know that isNumber is unused.


📦 Tree Shaking in Popular Libraries

Lodash

Lodash traditionally exposes functions as part of a single object:

import _ from 'lodash';

This import pulls in the entire library (~70kb gzipped). Even if you only use _.debounce, everything comes along.

Shakeable Alternative

import debounce from 'lodash/debounce';

Or better yet:

import { debounce } from 'lodash-es'; // ESM-compatible build

lodash-es allows true tree shaking because it uses native ESM exports.

D3

D3 is a famously large and modular library. The main d3 package is a bundle of ~30 submodules.

import * as d3 from 'd3';

This includes everything.

Instead:

import { scaleLinear } from 'd3-scale';

Modular imports enable bundlers to remove unused parts of D3.

React

React itself isn’t tree-shakeable in the same way because you typically import the whole API:

import React from 'react';

However, React’s internal dev-only code is removed via process.env.NODE_ENV checks during bundling (e.g., with Webpack + Babel). Also, React v18+ modular architecture (like react-dom/client) enables finer granularity.


✅ Best Practices for Writing Shakeable Code

  1. Use ESM syntax: Avoid require() and module.exports.

  2. Avoid barrel files: Re-exporting everything from index.js makes it harder for bundlers to analyze.

  3. Avoid deep object exports:

    export const utils = { a, b, c }; // bad
    export { a, b, c }; // good
    
  4. No side effects: Code with side effects can’t be safely removed.

    Example:

    import './styles.css'; // not tree-shakeable
    

    Add "sideEffects": false to your package.json (or list specific files).


🛠 Bundler Differences

Feature Webpack Rollup esbuild
Tree Shaking Yes Yes (very good) Yes (fastest)
ESM Support Partial (better in v5) Excellent Excellent
Side Effects Needs config (sideEffects) Auto detection Auto detection
Performance Medium Slow Extremely Fast

Each bundler’s approach and these examples illustrate both the power and the caveats of tree shaking in practice.