Tree Shaking: Eliminate Dead Code from Your Bundle

Easy

Tree shaking eliminates unused code automatically. Learn how to write tree-shakeable code and maximize bundle size reduction.

Quick Decision Guide

Quick Implementation Guide:

Use ES6 modules: Only import/export syntax enables tree shaking. CommonJS (require) doesn't work.

Named imports: import { specific } from 'library' instead of import * as lib from 'library'

Side-effect-free code: Mark packages as side-effect-free in package.json: "sideEffects": false

Direct imports: For lodash, use lodash-es (ES module version) not lodash

Verify results: Use webpack-bundle-analyzer to see what's included in your bundle.

How Tree Shaking Works

Static Analysis

Tree shaking relies on ES6 module syntax which is statically analyzable:

// Static imports (analyzable at build time)
import { functionA, functionB } from './utils';

// Only functionA is used
functionA();

// Result: functionB is removed from bundle

Build Process

1. Bundler analyzes imports: Tracks what's imported and used

2. Marks unused exports: Identifies dead code

3. Removes dead code: Excludes from final bundle

4. Minification: Further optimization

Example

// utils.js
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) { return a / b; }

// app.js
import { add, multiply } from './utils';
console.log(add(2, 3));

// Final bundle only includes:
// - add function
// - multiply is imported but unused (removed)
// - subtract and divide are never imported (removed)

Writing Tree-Shakeable Code

Use ES6 Modules

// ✅ Tree-shakeable (ES6)
export function debounce() { /* ... */ }
export function throttle() { /* ... */ }

// ❌ Not tree-shakeable (CommonJS)
module.exports = {
  debounce: function() { /* ... */ },
  throttle: function() { /* ... */ }
};

Named Exports vs Default

// ✅ Better: Named exports
export function getUser() { /* ... */ }
export function deleteUser() { /* ... */ }

import { getUser } from './api';
// Only getUser is bundled

// ⚠️ Less optimal: Default export
export default {
  getUser: () => { /* ... */ },
  deleteUser: () => { /* ... */ }
};

import api from './api';
api.getUser();
// Entire object is bundled even if deleteUser is unused

Avoid Side Effects

// ❌ Has side effects (can't be tree-shaken)
import './styles.css'; // Modifies global state
console.log('Module loaded'); // Runs on import

export function myFunction() { /* ... */ }

// ✅ No side effects (tree-shakeable)
export function myFunction() { /* ... */ }

Library-Specific Patterns

Lodash

// ❌ Bad: Imports entire library (70KB+)
import _ from 'lodash';
_.debounce(fn, 300);

// ⚠️ Better but not optimal
import { debounce } from 'lodash';
// Still imports some shared utilities

// ✅ Best: Use lodash-es
import { debounce } from 'lodash-es';
// Fully tree-shakeable

// ✅ Also good: Direct import
import debounce from 'lodash-es/debounce';

Material-UI / MUI

// ❌ Bad: Imports everything
import { Button, TextField, Dialog } from '@mui/material';

// ✅ Good: Direct imports (automatic tree-shaking)
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';

// ✅ Also good: Named imports (MUI v5+ tree-shakes automatically)
import { Button, TextField } from '@mui/material';

Date Libraries

// ❌ Avoid: Moment.js (not tree-shakeable, 290KB)
import moment from 'moment';

// ✅ Use: date-fns (fully tree-shakeable)
import { format, addDays } from 'date-fns';

// ✅ Or: Day.js (smaller alternative, 2KB)
import dayjs from 'dayjs';

Package Configuration

package.json - sideEffects

Tell bundlers which files have side effects:

{
  "name": "my-library",
  "sideEffects": false
}

sideEffects: false = "All files are side-effect-free, tree-shake aggressively"

Specific Side Effects

{
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/polyfills.js"
  ]
}

Only these files have side effects, everything else is tree-shakeable.

Module Fields

{
  "main": "dist/index.cjs.js",     // CommonJS (not tree-shakeable)
  "module": "dist/index.esm.js",   // ES6 (tree-shakeable)
  "exports": {
    ".": {
      "import": "./dist/index.esm.js",
      "require": "./dist/index.cjs.js"
    }
  }
}

Modern bundlers prefer the "module" or "exports.import" field.

Verifying Tree Shaking

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()
  ]
};

Vite Bundle Analysis

npm run build -- --mode analyze
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    })
  ]
};

Check Import Size

// Before optimization
import _ from 'lodash'; // 70KB

// After optimization
import { debounce } from 'lodash-es'; // 5KB

// Savings: 65KB (93% reduction)

Real Example

Before tree shaking:
  bundle.js: 850KB
  - lodash: 70KB
  - moment: 290KB  
  - unused utils: 100KB

After tree shaking:
  bundle.js: 390KB (54% reduction)
  - lodash (debounce only): 5KB
  - date-fns (format only): 2KB
  - unused code: removed

Best Practices

Do's

Use ES6 modules exclusively for libraries
Named exports over default exports
Import only what you need from libraries
Mark side-effect-free packages in package.json
Use tree-shakeable alternatives (lodash-es, date-fns)
Verify with bundle analyzer regularly

Don'ts

Avoid CommonJS (require/module.exports) in modern code
Don't import entire libraries when you need one function
Avoid barrel exports that re-export everything
Don't use moment.js (use date-fns or Day.js)

Common Mistakes

1. Mixing Module Systems

// ❌ Mixed (breaks tree shaking)
import { func1 } from './module1'; // ES6
const func2 = require('./module2'); // CommonJS

// ✅ Consistent ES6
import { func1 } from './module1';
import { func2 } from './module2';

2. Re-exporting Everything

// ❌ Barrel export (imports everything)
// index.js
export * from './module1';
export * from './module2';

// ✅ Named re-exports
export { specific, functions } from './module1';

3. Side Effects in Modules

// ❌ Side effect on import
// utils.js
console.log('Utils loaded');
export function add() { /* ... */ }

// ✅ No side effects
export function add() { /* ... */ }