Tree Shaking vs Dead Code Elimination: What Every Developer Should Know

Tree Shaking vs Dead Code Elimination: What Every Developer Should Know
Photo by Adam Kring / Unsplash

When developers talk about optimizing JavaScript bundles, two terms often pop up: tree shaking and dead code elimination. They sound similar, and both aim to reduce the final bundle size, but they work in different ways and at different stages. Understanding the difference helps you reason about why certain pieces of code are (or aren’t) removed, and what you can do to help your tools generate leaner output.


What is Tree Shaking?

Tree shaking is the process of removing unused exports from ES modules. The name comes from the analogy of “shaking a tree” so that dead leaves fall off — only the parts of the code tree you actually use remain.

  • How it works:
    Tree shaking relies on the static structure of ES modules (import and export). Bundlers like Webpack, Rollup, Vite, Bun, or Parcel analyze which exports are imported, and discard the rest during bundling.
  • Key limitation:
    Tree shaking works only when imports/exports can be analyzed statically. If you use CommonJS (require) or dynamic imports with unpredictable patterns, tree shaking might not be able to do its job.

Example:

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

// app.js
import { add } from "./math.js";
console.log(add(2, 3));

In this example, only add is imported, so subtract gets “shaken off” and won’t appear in the final bundle.


What is Dead Code Elimination?

Dead code elimination (DCE) is a more general optimization technique that removes code paths that can never be executed. This is not limited to unused exports — it can apply to any part of your program.

  • How it works:
    DCE depends on control-flow and data-flow analysis. Minifiers and compilers like Terser, UglifyJS, Closure Compiler, GCC, or LLVM identify unreachable code and strip it out.
  • Key strength:
    DCE goes beyond imports/exports. It eliminates unreachable logic, removes unused variables, folds constants, and simplifies conditional expressions.

Example:

if (false) {
  console.log("This will never run");
}

function alwaysFalse() { return false; }
if (alwaysFalse()) {
  console.log("Also removed");
}

Both blocks will be removed because the optimizer can prove they will never run.


How They Work Together

A typical modern JavaScript build pipeline looks like this:

  1. Bundler Stage (Tree Shaking):
    • Bundler analyzes ES module imports/exports.
    • Only the code you explicitly import survives.
    • Example: unusedFn is removed if it’s never imported.
  2. Minifier Stage (Dead Code Elimination):
    • Minifier strips unreachable code, removes unused variables, and compresses syntax.
    • Example: if (false) { … } is completely gone.

Together, tree shaking and dead code elimination complement each other. Tree shaking works at the module level, while DCE works at the execution logic level.


Additional Considerations

  • Side Effects:
    Some modules perform side effects when imported (e.g., polyfills that modify globals). Bundlers are careful not to shake these off unless you explicitly mark them as side-effect free in package.json ("sideEffects": false). Misconfiguring this can lead to broken behavior.
  • Dynamic Imports and CommonJS:
    Tree shaking works best with ESM (import/export). If you rely heavily on require(), dynamic property access, or runtime code generation, the bundler often can’t determine what’s unused.
  • Production Builds vs Development Builds:
    Tree shaking and DCE are usually applied in production builds, not in development. That’s why your development bundle may look bloated, but your production build is leaner.
  • Beyond JavaScript:
    The concepts aren’t limited to JS. C/C++ compilers use DCE. Languages with module systems (like Rust or Go) do their own form of tree shaking by eliminating unused imports or symbols.

Tree Shaking Isn’t Execution-Aware:
If you import something and never use it, tree shaking may still include it. For example:

import { bigFn } from "./utils.js";
// never called, but bundled

In this case, DCE may later strip it if the minifier can prove it’s unused.


Finally

Both tree shaking and dead code elimination aim to reduce bundle size, but they attack the problem from different angles:

  • Tree shaking is about unused exports at the module level.
  • Dead code elimination is about unreachable logic at the execution level.

When combined, they ensure that your final production code is as small and efficient as possible — provided your code and dependencies are structured in ways that allow these optimizations to work.

👉 As a developer, the best thing you can do is:

  • Prefer ES modules over CommonJS.
  • Mark packages correctly for side effects.
  • Avoid patterns that make static analysis impossible.

That way, both tree shaking and dead code elimination can work to their fullest potential.

Support Us