JavaScript Dead Code Elimination in UglifyJS and Terser
Dead code elimination (DCE) is an optimization technique that removes unreachable, unused, or redundant code from an application to reduce file size and improve load times. Modern JavaScript minifiers like UglifyJS and Terser perform this process by parsing source code into an Abstract Syntax Tree (AST), analyzing control flows and variable usages, pruning unnecessary AST nodes, and generating a compressed output file.
What Is Dead Code Elimination?
Dead code elimination targets two primary categories of code:
- Unreachable Code: Code that can never be executed
under any runtime condition (e.g., code following a
returnorthrowstatement, or statements inside anif (false)block). - Unused Code (Dead Stores): Variables, functions, or expressions that are defined or evaluated but never referenced elsewhere, provided their evaluation creates no side effects.
By stripping this code before production deployment, developers reduce bundle sizes, decrease memory consumption, and speed up JavaScript parsing and execution times in the browser or runtime environment.
How UglifyJS and Terser Process ASTs
Both UglifyJS and Terser (a modern fork of UglifyJS optimized for ES6+) rely on AST manipulation rather than regular expressions or string matching to ensure code transformations are semantically safe. The elimination process occurs across distinct phases:
1. Parsing Code into an AST
The minifier reads the JavaScript source code and converts it into a
tree data structure (AST). Each node in the tree represents a syntactic
construct, such as an IfStatement,
BinaryExpression, FunctionDeclaration, or
Identifier.
2. Scope and Symbol Analysis
Terser and UglifyJS traverse the AST to build a symbol table. This step maps variables and functions to their respective lexical scopes. The minifier tracks: * How many times an identifier is referenced. * Whether a variable is reassigned. * Whether a function is invoked or exported.
If an identifier has zero references and does not escape its scope, its corresponding AST node becomes a candidate for removal.
3. Constant Folding and Conditional Evaluation
Minifiers evaluate static expressions at compile time (known as constant folding). For example:
if (DEBUG && 1 + 1 === 2) {
console.log("Debugging");
}If DEBUG is set to false, the condition
statically evaluates to false. The minifier converts the
AST IfStatement node into an empty block or eliminates the
node entirely, preventing the console.log branch from
appearing in the output.
4. Control Flow Pruning
The AST traversal inspects execution flow boundaries. When a jump
statement—such as return, throw,
break, or continue—is encountered within a
block, any subsequent sibling nodes in that block are marked
unreachable. The tree transformer splices these dead nodes out of the
AST body.
5. Side-Effect Analysis (Purity Checking)
Before dropping unused expressions or function calls, the minifier checks for potential side effects.
- Safe to drop: Primitive assignments, pure
arithmetic (
const x = 5 * 10;), and functions annotated with/*@__PURE__*/comments. - Unsafe to drop: Function calls that might alter
global state, modify DOM elements, or throw exceptions (e.g.,
doSomething();wheredoSomethingcannot be verified as pure).
If an expression contains both side effects and unused values, Terser
optimizes the AST by retaining only the side-effect portion (e.g.,
transforming const x = getVal() into
getVal()).
6. AST Reconstruction and Code Generation
After multiple compression and tree-shaking passes, the modified AST contains only the nodes necessary for execution. The code generator traverses the final, pruned AST to produce the final, minified JavaScript string.