Javascript is required
TroubleshootingPublished 2026-07-28Updated 2026-08-085 min read

Terser: Dead Code, Side Effects and Pure Annotations

In modern JavaScript build pipelines, bundlers handle module graph tree-shaking while minifiers optimize AST-level Dead Code Elimination (DCE) and variable mangling. When unreferenced functions or initialization logic are unexpectedly erased or retained, the root cause lies in side-effect analysis. This article clarifies roles and side-effect marking.

By: Tiny
TerserJS MinificationSide EffectsTree Shaking Bug

1. Problem: overlapping and misunderstood responsibilities of bundlers and minifiers

Developers frequently blame Terser or esbuild when tree-shaking fails or code is stripped. In reality, bundlers like Rollup or Webpack determine which modules and exports belong in the bundle, whereas minifiers like Terser decide which unreferenced variables or function calls within an AST can be safely erased.

2. Minimal reproduction: unreferenced function calls retained or stripped without PURE markers

In the snippet below, createLogger only instantiates an object without modifying global scope. However, without a PURE annotation, a minifier cannot safely discard the call.

/* Source logic: unreferenced variable assignment */
const logger = createLogger({ level: 'info' });

/* Without PURE annotation: minifier keeps expression assuming potential global side effects */
createLogger({ level: 'info' });

/* With PURE annotation: minifier safely identifies call as side-effect free and erases it */
const logger = /*@__PURE__*/ createLogger({ level: 'info' });

3. Root cause: limits of dynamic JavaScript side-effect inference

JavaScript is highly dynamic: global modifications, property getters, Object.defineProperty, and class constructors can trigger runtime side effects. Terser employs conservative semantic analysis, keeping calls unless purity can be proven.

4. Recommendation: explicit /*@__PURE__*/ annotations and library design rules

1. Library design: prepend /*@__PURE__*/ or /*#__PURE__*/ comments before top-level object instantiations or helper calls. 2. Application code: avoid top-level global mutations; encapsulate initialization inside explicit init() calls. 3. Configure Terser compress.pure_funcs and side_effects options appropriately.

5. Complete code: simulating minifier side-effect evaluation and DCE logic

This TypeScript snippet demonstrates how a minifier evaluates variable references, global modification flags, and PURE annotations to decide DCE erasure.

interface FunctionCallNode {
  name: string;
  hasPureAnnotation: boolean;
  modifiesGlobal: boolean;
}

function evaluateDeadCodeElimination(node: FunctionCallNode, isReferenced: boolean): boolean {
  if (isReferenced) return false;
  if (node.hasPureAnnotation && !node.modifiesGlobal) return true;
  return false;
}

const callA: FunctionCallNode = { name: "createLogger", hasPureAnnotation: true, modifiesGlobal: false };
const callB: FunctionCallNode = { name: "setupGlobalState", hasPureAnnotation: false, modifiesGlobal: true };

console.log(evaluateDeadCodeElimination(callA, false)); // true (safely erases pure call)
console.log(evaluateDeadCodeElimination(callB, false)); // false (conservatively retains side-effectful call)

6. Incorrect approaches

Enabling aggressive unsafe Terser options indiscriminately, breaking stateful functions. Relying on top-level global mutations while expecting minifiers to erase them. Expecting minifiers to perform multi-file tree-shaking alone.

7. Boundaries: IIFEs and getter interceptions

Global mutations inside IIFEs cannot be automatically tracked. Property getter and setter evaluations are always treated as potentially side-effectful.

8. Verification

Inspect bundle output to verify target code is removed. Run identical initialization, side-effect, and dynamic-import tests against unminified and minified builds, then compare critical runtime output.

9. FAQ

How does bundler tree-shaking differ from minifier DCE? Bundlers eliminate unused module exports via ES Module import graphs; minifiers discard unreferenced AST expressions and local variables.

Is /*@__PURE__*/ supported across all tools? Yes, Terser, esbuild, Rollup, and SWC support this standard annotation.

Why was a PURE-annotated call still retained? If its return value is referenced by active code or arguments contain non-pure expressions, the call must be kept.

10. Summary

Separate bundler module graph tree-shaking from minifier AST DCE. Explicitly annotate side-effect-free calls with PURE and avoid top-level global mutations for optimal JavaScript compression.

Sources and further reading

Specifications and references for checking the article’s technical details.

Related articles

Keep exploring

Use the related browser tool to test the workflow with your own sample data.

Open related tool