Understanding Tree Shaking, Dead Code Elimination, and Side Effects in JavaScript Bundlers
Tree shaking, a subset of dead‑code elimination based on ES2015 module syntax, removes unused top‑level code, while side‑effect analysis determines which modules can be safely omitted; this article explains the terminology, algorithms, safety vs optimization trade‑offs, and practical debugging steps for modern JavaScript bundlers.
Background
During the development of a custom bundling tool, we observed that different compilation tools can produce vastly different bundle sizes for the same code. The main reason is the varying implementations of code‑optimization techniques, especially tree shaking, which lacks a standardized definition across tools.
Terminology
minify : Reduces program size without changing semantics; common tools include terser, uglify, swc, and esbuild.
Dead Code Elimination (DCE) : Removes code that does not affect program results; techniques include constant folding, control‑flow analysis, and link‑time optimization (LTO).
Link‑Time Optimization (LTO) : Performs cross‑module analysis during the linking phase, allowing removal of unused exports and symbol mangling.
Tree shaking : A JavaScript‑specific dead‑code elimination technique that relies on ES2015 module syntax. It removes unused top‑level statements across modules (LTO‑based DCE) but does not perform intra‑function DCE.
mangle : Shortens variable and function names.
side effect : Code that changes program state; dead‑code elimination must preserve side‑effectful statements even if their results are unused.
module‑internal side effect : A side effect that only matters within the module itself; if no other module imports the module’s exports, the whole module can be dropped.
Safety vs. Optimization Level
Comparing tools such as Rollup and esbuild shows that tree‑shaking algorithms are relatively stable, but DCE implementations differ, leading to trade‑offs between safety and compression ratio. For example, Rollup may safely remove an unused object, while esbuild keeps it to avoid breaking code that relies on side effects like Object.defineProperty on Object.prototype .
Why Tree Shaking Is Separate from General DCE
It skips parsing and loading of modules whose exported symbols are never referenced, reducing build overhead.
It can use module‑level side‑effect metadata to decide whether an entire module can be omitted.
It preserves the relationship between assets (e.g., image loaders) and the code that imports them, which pure DCE would lose.
Tree Shaking Algorithm
Without sideEffect
Preserve Referenced Export Statements
const secret = 10; // stmt1
export const answer = 42; // stmt2Only answer is exported, so the final output keeps just the export.
export const answer = 42;Cross‑Module Reference Analysis
// index.js
import { answer } from './lib';
export { answer }; // lib.js
export const answer = 42;
export const secret = 10;Since secret is never used, it is removed.
export const answer = 42;Deep Analysis of Re‑exports
// index.js
import { answer } from './lib';
export { answer }; // lib.js
export * from './internal'; // internal.js
export const answer = 42;
export const secret = 10;The algorithm follows the re‑export chain and keeps only the reachable answer definition.
export const answer = 42;Symbol Association
// index.js
import { answer } from './lib';
export { answer }; // lib.js
export let secret = 10;
export const answer = secret + 32;
export * from './reexport'; // reexport.js
export const internal = 100;
console.log('sideEffect');Even though secret is not directly imported, it is needed for the computation of answer , so it remains.
Side Effects
Side effects affect the algorithm in two ways:
The entry set includes not only exported statements but also side‑effect statements (e.g., console.log('secret', secret) forces secret to be retained).
Modules themselves may contain side effects; therefore, even if a module’s exports are unused, the module must still be analyzed for side‑effect code.
Compilers often use /*# PURE */ annotations to explicitly mark statements as side‑effect‑free.
Side‑Effect Optimization
Webpack’s sideEffects field marks a module as having internal side effects. If a module’s internal side effects are not needed (no import of its exports), the whole module can be dropped, similar to Rust’s internal mutability concept.
sideEffects Field Example
// index.js
import { answer } from './lib'; // lib.js
export const answer = 42;
export const secret = 10;
console.log('lib');
export * from './reexport'; // reexport.js
console.log('internal');
export const internal = 100;With sideEffects: false , the reexport.js code is removed because its side effects are internal and no other module imports internal . The lib.js side effect remains because answer is used.
Re‑export Mechanism
Webpack treats both export * from '...' and named re‑exports (e.g., export { internal } from './reexport' ) as re‑exports, while esbuild only recognises the wildcard form.
Tree Shaking vs. DCE
Most tools perform tree shaking during the module‑link phase and DCE during the bundle‑print phase, so the results of one stage are not usually fed into the other.
Summary
In production, minification performs DCE, and tree shaking is a module‑level DCE based on symbol analysis. Because JavaScript’s dynamic nature makes side‑effect detection hard, tools use pure annotations and the sideEffects field to improve accuracy and allow developers to control which modules can be safely eliminated.
Common Misconceptions
Setting sideEffects: false does not mean the code has no side effects; it means the side effects are internal to the module.
Applying sideEffects: false to CSS files can unintentionally drop required styles; CSS side‑effect handling should be tied to the component that imports it.
Tree Shaking Troubleshooting Steps
Determine whether the issue is DCE‑related or tree‑shaking‑related by checking if the code appears at top‑level or inside functions.
If DCE fails, examine the terser/esbuild configuration (e.g., increase passes ).
If tree shaking fails, verify module path information (disable minify to keep module metadata).
Check whether the module has side effects and whether it is an ES module; you can temporarily set sideEffects: false to see the impact on bundle size.
If size does not change after disabling side effects, either the module is still referenced somewhere, or the bundler has a bug; further investigation or contacting the tool’s maintainers may be required.
- END -
ByteDance Web Infra
ByteDance Web Infra team, focused on delivering excellent technical solutions, building an open tech ecosystem, and advancing front-end technology within the company and the industry | The best way to predict the future is to create it
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.