Tree Shaking vs Dead Code Elimination
Tree shaking analyzes a module graph to find exports that the application does not use. Dead code elimination (DCE) removes code that the build tool can prove has no effect on the program's result.
Application entry points
↓
Analyze imports and exports
↓
Mark reachable code
↓
Remove unused modules, exports and statements
↓
Smaller production output
One-line idea: Tree shaking finds unused code through module relationships, while dead code elimination removes code proven unnecessary; tree shaking is commonly treated as a module-level form of DCE.
Core Concepts
Why Unused JavaScript Matters
Applications often depend on libraries or modules that expose more functionality than one page needs. If all of that code reaches the browser, users must download, parse and possibly compile JavaScript that never contributes to the interface.
Removing unused code can improve:
- production bundle size
- download time on slower networks
- JavaScript parsing and compilation work
- cache efficiency
- startup performance
Tree shaking and DCE support the same goal, but they discover removable code in different ways.
What Is Tree Shaking?
Tree shaking starts from the application's entry points and follows the module graph. Exports that cannot be reached from the used imports can be excluded from the final output.
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
import { add } from "./math.js";
console.log(add(2, 3));
Because app.js uses add but not subtract, a production build may omit subtract.
app.js uses add
↓
add is reachable
subtract is not reachable
↓
Keep add, remove subtract
The term comes from imagining the dependency graph as a tree: used branches stay connected to an entry point, while unused branches can be shaken off.
Why ES Modules Help
ES module imports and exports have a static structure that tools can analyze before running the program.
import { formatDate } from "./date-utils.js";
export { formatDate };
The imported and exported names are visible in the syntax. Patterns whose dependencies are determined at runtime can be harder to analyze safely:
const utilities = require(moduleName);
CommonJS is not automatically impossible to optimize, and ES modules are not automatically tree-shakeable. The result depends on the build tool, package format, configuration and whether evaluating a module causes side effects.
What Is Dead Code Elimination?
DCE is the broader compiler or minifier optimization that removes code proven to be unreachable, unused or without an observable effect.
if (false) {
sendDebugMessage();
}
function calculateTotal() {
return 100;
console.log("unreachable");
}
The false branch can never run, and the statement after return is unreachable. A production optimizer can remove both.
DCE can also work after other transformations expose dead code:
const isDevelopment = false;
if (isDevelopment) {
enableDebugTools();
}
After replacing or evaluating the constant, the optimizer can prove that the debug branch is unused.
Reachability and Observability
Code is not removable simply because its return value is ignored. The tool must preserve anything that could change observable behavior.
initializeAnalytics();
Even when the call's result is unused, it may register listeners, send a request or modify global state. Removing it could change the application.
Optimizers are conservative: when they cannot prove that removal is safe, they normally keep the code.
Tree Shaking vs Dead Code Elimination
| Area | Tree Shaking | Dead Code Elimination |
|---|---|---|
| Main focus | Unused modules and exports | Unreachable or effect-free code |
| Main input | Module dependency graph | Program statements and expressions |
| Typical scope | Across files and packages | Within and across compiled modules |
| Common example | An exported helper is never imported | A branch is always false |
| Key requirement | Statically understandable dependencies | Proof that removal preserves behavior |
| Typical tool | Bundler | Compiler or minifier |
The boundary is not strict. Tool documentation often uses tree shaking as a JavaScript term for dead code elimination. In a real production pipeline, a bundler may mark unused exports and a minifier may perform the final removal.
How Production Optimization Works
Module Graph Analysis
A bundler begins with one or more entry points and follows their dependencies:
entry.js
├── imports renderPage from ui.js
│ ├── renderPage -> used
│ └── renderPreview -> unused
└── imports config.js -> evaluated for setup
Unused exports may be omitted, but a module that performs required setup must still be evaluated even when none of its exports are read.
Side Effects Matter
A side effect is observable work performed while evaluating code, beyond returning a value or exposing exports.
window.analyticsQueue = window.analyticsQueue || [];
window.addEventListener("error", recordError);
import "./analytics.js";
This side-effect-only import must be preserved. Removing it would change runtime behavior.
Some packages provide a sideEffects hint in package.json:
{
"sideEffects": ["*.css", "./src/analytics.js"]
}
This tells compatible tools which files must not be discarded as side-effect-free. Setting "sideEffects": false incorrectly can remove styles, polyfills, registrations or initialization code from a production build.
Pure Calls and Annotations
An optimizer may keep a function call because it cannot prove that the call has no side effects:
const unusedValue = createLookupTable();
Some toolchains understand a /*#__PURE__*/ annotation that declares a call removable when its result is unused:
const unusedValue = /*#__PURE__*/ createLookupTable();
Use this only when the call is genuinely free of side effects. A wrong annotation can cause required behavior to disappear from production.
Tree Shaking and Minification
Tree shaking, DCE and minification are related but not identical:
Bundler analyzes the module graph
↓
Unused exports are marked or excluded
↓
Optimizer removes dead statements
↓
Minifier rewrites remaining code compactly
Minification also shortens names, removes whitespace and simplifies syntax. A tool may perform DCE during its compression phase, but making code shorter and proving code unnecessary are different operations.
Practical Patterns
Prefer Direct Named Imports
Use imports that make the required bindings clear:
import { debounce } from "utility-library";
However, syntax alone does not guarantee a small bundle. The package must expose optimizable modules, and the bundler must be able to understand them.
Replace Build-Time Flags
Production builds commonly replace environment constants so that development-only branches become removable:
if (process.env.NODE_ENV !== "production") {
showDebugPanel();
}
After the build tool replaces the condition with a known value, DCE can remove the branch from production output.
Preserve Module Syntax Until Bundling
If an earlier compilation step converts ES modules into a less statically analyzable format, the bundler may lose useful information. Configure transpilation so the bundler receives the module syntax it expects.
Verify the Production Bundle
Do not judge tree shaking from a development build. Development output often keeps module wrappers, names and unused code for faster builds and easier debugging.
Verify the optimized production build by:
- comparing bundle sizes before and after a change
- using the bundler's module or bundle analyzer
- searching generated output for a distinctive unused function
- testing features that rely on side-effect imports
- checking both initial and lazy-loaded chunks
Benefits, Limitations and Common Mistakes
Benefits
- Reduces unused JavaScript in production.
- Helps libraries expose broad APIs without forcing every consumer to ship all of them.
- Can remove development-only and impossible code paths.
- Works automatically when code and package metadata are optimizer-friendly.
Limitations
- Dynamic dependency patterns reduce certainty.
- Side effects can require whole modules or statements to remain.
- Re-export files can make analysis more complicated for some toolchains.
- Reflection, getters and global mutations are difficult to reason about safely.
- Small source changes do not always produce meaningful transfer savings after compression.
| Mistake | Better approach |
|---|---|
| Saying tree shaking and DCE are completely unrelated | Describe tree shaking as module-graph-driven dead code removal |
| Assuming every named import produces a tiny bundle | Inspect the package output and final production chunks |
Setting sideEffects: false without auditing files | Preserve CSS, polyfills and initialization modules explicitly |
| Expecting development bundles to be fully optimized | Verify an optimized production build |
| Confusing tree shaking with code splitting | Tree shaking removes code; code splitting moves code into separate chunks |
| Confusing DCE with minification | DCE removes unnecessary behavior; minification compacts what remains |
| Adding pure annotations to effectful calls | Annotate only calls whose removal cannot change behavior |
| Measuring only raw bundle size | Also compare compressed transfer size and runtime cost |
Official References
Summary
Tree shaking removes unreachable parts of the module graph, while dead code elimination removes any code proven unnecessary; production toolchains commonly use both together.
Start from application entry points
↓
Follow statically known module references
↓
Preserve reachable code and required side effects
↓
Remove unused exports and unreachable statements
↓
Minify, build and inspect the final chunks