Tree Shaking vs Minification: Reducing JavaScript Bundle Size

May 28, 20267 min read

What Is Tree Shaking?

Tree shaking is a dead code elimination technique that removes exports your application never uses. The name comes from the mental image of shaking a tree and letting the dead leaves fall off.

Unlike minification — which shortens code but keeps all of it — tree shaking actually deletes unused functions, classes, and variables from your bundle. The two techniques work together: tree shaking removes dead code, then minification compresses what remains.

Why ES Modules Matter

Tree shaking depends on ES module syntax (import and export). The reason is static analysis.

// ES modules — statically analyzable
import { debounce } from 'lodash-es';
export function formatDate(date) { ... }

// CommonJS — dynamic and unpredictable
const { debounce } = require('lodash');
module.exports = { formatDate: function(date) { ... } };

ES module imports are resolved at compile time. The bundler can see exactly which exports a module provides and which ones your code imports. If you import debounce but not throttle, the bundler knows throttle is safe to drop.

CommonJS require() calls are dynamic — the argument can be a variable, a conditional, or a computed string. The bundler cannot reliably determine what gets used, so it keeps everything.

Rule: If you want tree shaking to work, use import/export throughout your codebase.

How Tree Shaking Works

The process follows three steps:

  1. Build the dependency graph — The bundler starts at your entry point and follows every import statement to build a complete map of modules and their exports.
  2. Mark used exports — For each module, the bundler checks which exports are actually referenced by other modules in the graph. Unreferenced exports get marked as dead.
  3. Eliminate dead code — The bundler removes the marked exports from the output. A minifier then cleans up any orphaned code left behind.
// utils.js — three exports
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }

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

The bundler includes add and removes multiply and divide — they are never imported.

Tree Shaking vs Dead Code Elimination

These terms overlap but are not identical:

Tree ShakingDead Code Elimination
ScopeModule-level exportsAny unreachable code
MechanismUnused export removalUnreachable branch removal
ExampleRemoving an exported function you never importRemoving code inside if (false) { ... }

Tree shaking operates on the module graph. Dead code elimination operates within individual functions and blocks. Both reduce size, and modern bundlers apply both automatically.

Side Effects and Package.json

Some modules execute code simply by being imported — even if you never use their exports. These are side effects.

// side-effect.js
window.myGlobal = 'hello';

Importing this file changes global state. A bundler must keep it, even if no export is used.

To help bundlers optimize correctly, library authors add a sideEffects field to package.json:

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

This declaration tells the bundler: "No file in this package has side effects. Drop any unused exports freely."

If only certain files have side effects:

{
  "sideEffects": ["./src/polyfills.js", "./src/global-setup.js"]
}

For consumers: Check whether your dependencies declare sideEffects: false. Libraries like lodash-es, date-fns, and ramda do — and that is why they tree-shake well.

Bundler Configuration

Webpack

// webpack.config.js
module.exports = {
  mode: 'production', // enables tree shaking + Terser
  optimization: {
    usedExports: true,        // mark unused exports
    minimize: true,           // run Terser
    concatenateModules: true, // scope hoisting
  },
};

Webpack's tree shaking activates in production mode by default. The usedExports flag tells Webpack to track which exports are used and drop the rest during minification.

Rollup

// rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm',
  },
  treeshake: {
    moduleSideEffects: false, // assume no side effects
  },
};

Rollup was built for ES modules and tree shakes by default. It often produces smaller bundles than Webpack for library code because its scope hoisting is more aggressive.

Vite

// vite.config.js
export default {
  build: {
    rollupOptions: {
      treeshake: true, // default in production
    },
  },
};

Vite uses Rollup for production builds, so tree shaking works out of the box. No extra configuration needed for most projects.

Tree Shaking and Minification Together

These two techniques are complementary, not competing:

  1. Tree shaking runs first — it removes entire unused modules and exports
  2. Minification runs second — it compresses the remaining code by shortening names and removing whitespace

The order matters. If you minify first, variable names get shorter but the dead code is still there. Tree shaking then removes the dead code, but the minifier has already wasted effort on code that gets deleted.

In practice, you do not control this order — your bundler does. Just make sure both are enabled in production mode.

Common Pitfalls

  • Importing entire librariesimport _ from 'lodash' imports everything and defeats tree shaking. Use import { debounce } from 'lodash-es' instead
  • Re-exporting everythingexport * from './module' forces the bundler to keep all exports because it cannot know which ones downstream consumers need
  • Dynamic importsimport('./module') creates a separate chunk; the bundler cannot tree-shake across dynamic boundaries
  • CommonJS dependencies — If a dependency uses module.exports, the bundler may conservatively include the entire module
  • Missing sideEffects field — Without sideEffects: false in package.json, bundlers must assume every file might have side effects

Key Takeaways

  • Tree shaking removes unused ES module exports; minification compresses what remains
  • Static import/export syntax is required — CommonJS defeats tree shaking
  • Declare sideEffects: false in your package.json to help bundlers optimize aggressively
  • Webpack, Rollup, and Vite all support tree shaking in production mode by default
  • Import only what you need: named imports over namespace imports

Try It Yourself

Test the impact of tree shaking by minifying your JavaScript with our free JS Minifier. See how much smaller your code becomes when unused functions are stripped before compression.