TechLead
Lesson 8 of 22
5 min read
Performance Engineering

Tree Shaking

Eliminate dead code from your bundles with ES modules, sideEffects configuration, and proper import patterns

What Is Tree Shaking?

Tree shaking is a dead code elimination technique that removes unused exports from your JavaScript bundles during the build process. The term comes from the idea of "shaking the tree" to let dead leaves (unused code) fall away. Tree shaking relies on the static structure of ES module import and export statements to determine which exports are used.

Requirements for Tree Shaking

  • ES modules: Tree shaking only works with import/export, not require/module.exports
  • Static analysis: Imports must be statically analyzable (no dynamic require)
  • Side-effect-free: Modules must be marked as side-effect-free for aggressive elimination
  • Production mode: Tree shaking typically only applies in production builds

How Tree Shaking Works

// utils.ts — A utility module with multiple exports
export function formatDate(date: Date): string {
  return date.toLocaleDateString();
}

export function formatCurrency(amount: number, currency: string = 'USD'): string {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}

export function formatPercentage(value: number): string {
  return new Intl.NumberFormat('en-US', { style: 'percent', minimumFractionDigits: 1 }).format(value);
}

export function slugify(text: string): string {
  return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
}

// Only formatCurrency is imported — tree shaking removes the rest
// page.tsx
import { formatCurrency } from '@/utils';

function Price({ amount }: { amount: number }) {
  return <span>{formatCurrency(amount)}</span>;
}

// After tree shaking, the bundle only contains formatCurrency
// formatDate, formatPercentage, slugify are eliminated

Preventing Tree Shaking Failures

// ANTI-PATTERNS that prevent tree shaking

// 1. Barrel exports that re-export everything
// components/index.ts
export { Button } from './Button';
export { Modal } from './Modal';      // 50KB
export { DataTable } from './DataTable'; // 100KB
export { Chart } from './Chart';       // 200KB

// Importing Button pulls in ALL components in some bundlers
import { Button } from '@/components';  // May include 350KB!

// FIX: Import directly from the source file
import { Button } from '@/components/Button'; // Only Button code

// 2. CommonJS modules cannot be tree-shaken
const lodash = require('lodash'); // Entire library included
lodash.debounce(fn, 300);

// FIX: Use ES module imports
import { debounce } from 'lodash-es'; // Only debounce included

// 3. Side effects in module scope
// analytics.ts
console.log('Analytics module loaded'); // Side effect!
export function track(event: string) { /* ... */ }

// The bundler cannot remove this module even if track() is unused
// because the console.log runs at import time

// 4. Dynamic property access prevents static analysis
import * as utils from '@/utils';
const fn = utils[dynamicKey]; // Bundler must include ALL exports

Configuring sideEffects

// package.json — Mark your package as side-effect-free
{
  "name": "my-app",
  "sideEffects": false
  // This tells the bundler: "Every file in this package is side-effect-free.
  // If an export is unused, you can safely remove the entire module."
}

// If some files DO have side effects (CSS imports, polyfills):
{
  "name": "my-app",
  "sideEffects": [
    "**/*.css",
    "**/*.scss",
    "./src/polyfills.ts",
    "./src/register-service-worker.ts"
  ]
}

// next.config.js — Webpack optimization settings
const nextConfig = {
  webpack: (config, { isServer }) => {
    // Enable more aggressive tree shaking
    config.optimization = {
      ...config.optimization,
      usedExports: true,
      providedExports: true,
      sideEffects: true,
      innerGraph: true,
    };

    return config;
  },
};

Verifying Tree Shaking Works

// Analyze your bundle to verify tree shaking
// Install: npm install -D @next/bundle-analyzer

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // your next config
});

// Run: ANALYZE=true npm run build
// This opens a visual treemap of your bundles

// Programmatic bundle size checking
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';

interface BundleInfo {
  file: string;
  raw: number;
  gzipped: number;
}

function analyzeBundles(buildDir: string): BundleInfo[] {
  const jsDir = path.join(buildDir, 'static', 'chunks');
  const files = fs.readdirSync(jsDir).filter(f => f.endsWith('.js'));

  return files.map(file => {
    const filePath = path.join(jsDir, file);
    const content = fs.readFileSync(filePath);
    const gzipped = zlib.gzipSync(content);

    return {
      file,
      raw: content.length,
      gzipped: gzipped.length,
    };
  }).sort((a, b) => b.gzipped - a.gzipped);
}

const bundles = analyzeBundles('.next');
bundles.forEach(b => {
  console.log(`${b.file}: ${(b.raw/1024).toFixed(1)}KB raw, ${(b.gzipped/1024).toFixed(1)}KB gzipped`);
});

Tree Shaking Best Practices

  • Use ES modules: Always use import/export instead of require/module.exports
  • Import specifically: Import only the functions/components you need
  • Avoid barrel files: Import directly from source files instead of index.ts re-exports
  • Mark sideEffects: Set sideEffects: false in package.json for pure modules
  • Prefer tree-shakable libs: Use lodash-es over lodash, date-fns over moment
  • Verify with bundle analysis: Regularly check that unused code is actually removed

Continue Learning