参考链接
1. esbuild
2. terser
前言
Terser 为什么比 esbuild 压缩体积更小?原理分析
terser 之所以比 esbuild 产生的打包体积更小,主要是因为 它提供了更高级的优化手段,包括 作用域折叠(Scope Hoisting)、变量提升、代码混淆、AST 级别优化、更多高级压缩策略,而 esbuild 的压缩主要是 简单的语法转换和删除无用空格/换行符,缺少深入的 AST 级别优化。
下面我们从 代码优化原理、构建方式、作用域分析、Tree Shaking 等方面进行深入分析。
1. AST 层面的压缩差异
AST(Abstract Syntax Tree)抽象语法树
terser 和 esbuild 都基于 AST(抽象语法树)进行代码优化,但 terser 在 AST 级别执行了更多高级优化,而 esbuild 主要做基本的 minify(缩小代码)。
示例
假设我们有这样一段 JavaScript 代码:
function add(a, b) {return a + b;
}console.log(add(1, 2));
Esbuild 压缩
详细分析
1. 词法分析(Lexical Analysis)
esbuild的lexer会解析代码,将其转换成 Token 流,例如:function → Token{Type: FUNCTION} add → Token{Type: IDENTIFIER} ( → Token{Type: PUNCTUATION} a → Token{Type: IDENTIFIER} b → Token{Type: IDENTIFIER} return → Token{Type: RETURN} a + b → Token{Type: BINARY_EXPRESSION}- 这个过程只是将代码拆分成可解析的最小单元。
2. 语法解析(Parsing)
esbuild在js_parser中将 Token 解析为 AST(抽象语法树):{"type": "FunctionDeclaration","name": "add","params": ["a", "b"],"body": {"type": "ReturnStatement","argument": {"type": "BinaryExpression","operator": "+","left": "a","right": "b"}} }console.log(add(1, 2))也被解析成 AST 结构。
3. 代码优化(Minification)
在 js_printer 处理阶段:
- 移除空格、换行
- 保留
function add结构(因为esbuild不是一个高级压缩工具) - 不会执行函数折叠(不会把
add(1,2)直接计算成3)
所以最终输出:
function add(a,b){return a+b}console.log(add(1,2));
为什么 esbuild 没有优化成 console.log(3);?
相比 terser,esbuild 不会执行高级优化,例如:
- 常量折叠(Constant Folding):计算
add(1, 2)并替换成console.log(3)。 - 函数内联(Inlining):如果
add()只在一个地方调用,它可以被展开成console.log(1 + 2)。 - Dead Code Elimination(DCE,死代码消除):如果
add()没有被调用,它会直接删除add()。
terser 会进行这些优化:
terser发现add(1,2)这个调用是 纯函数(pure function),可以直接计算并替换为console.log(3),去掉add函数。
console.log(3);
关键优化点:
terser直接 折叠函数调用结果(常量折叠),消除了add()这个函数,从而减少代码体积。
2. Terser 的高级优化机制
1) 作用域折叠(Scope Hoisting)
作用域折叠可以 减少作用域的嵌套,将函数或变量合并到更紧凑的作用域中。
示例
原始代码:
function outer() {function inner() {console.log('Hello');}return inner;
}
outer()();
esbuild 压缩
function outer(){return function(){console.log("Hello")}}outer()();
- 依然保留了
outer这个作用域。
terser 压缩
console.log("Hello");
terser直接移除了outer,因为它的唯一作用是返回inner,可以省略掉。
作用:
- 减少作用域层级,降低闭包开销,提高运行时性能。
2) 变量提升和合并
terser 会尽可能减少变量声明的数量,合并多个变量定义,从而减少代码大小。
示例
原始代码:
let a = 1;
let b = 2;
let c = a + b;
console.log(c);
esbuild 压缩
let a=1,b=2,c=a+b;console.log(c);
esbuild只是合并了let语句,但没有进一步优化。
terser 压缩
console.log(3);
terser发现c是常量,直接替换掉,省略了变量声明。
3) 常量折叠(Constant Folding)
terser 能够分析并消除计算结果为常量的代码,而 esbuild 没有类似的优化。
示例
原始代码:
const a = 100;
const b = a * 2;
console.log(b);
esbuild 压缩
const a=100,b=a*2;console.log(b);
terser 压缩
console.log(200);
terser发现b是常量,直接用200替换掉b,从而减少代码体积。
4) 死代码消除(Dead Code Elimination)
terser 可以彻底删除不会被执行的代码,而 esbuild 只能删除一些最基本的未引用变量。
示例
原始代码:
function test() {if (false) {console.log("This will never run");}
}
test();
esbuild 压缩
function test(){}test();
esbuild只删除了if语句,但test()这个函数还在。
terser 压缩
terser发现test()没有任何作用,直接删掉整个函数调用。
5) Tree Shaking(摇树优化)
Tree Shaking 主要是用于删除未使用的模块,terser 在这方面比 esbuild 更激进。
示例
原始代码:
import { unusedFunc, usedFunc } from './module.js';
usedFunc();
esbuild 压缩
import { usedFunc } from "./module.js"; usedFunc();
esbuild只是去掉了unusedFunc的导入,但代码本身依然保留import语句。
terser 压缩
import { usedFunc } from "./module.js"; usedFunc();
terser在与rollup结合时,可以进一步优化 整个import语句,如果usedFunc也可以被内联,则可能直接删除import。
3. esbuild 的设计取舍
esbuild 的目标是快速打包,而不是极致的压缩,因此做了一些权衡:
- 不做复杂的 AST 分析(导致一些优化缺失)
- 不执行复杂的代码混淆(可读性更强)
- 优先优化构建速度(10~100 倍快)
相比之下,terser 作为一个专业的压缩器,使用了:
- 深度 AST 分析
- 更激进的 Tree Shaking
- 代码混淆
- 优化作用域和常量折叠
这些优化让 terser 产出的代码体积更小,但牺牲了构建速度。
4. 总结
| 特性 | esbuild | terser |
|---|---|---|
| 构建速度 | 快(10-100 倍) | 慢 |
| 代码压缩 | 基本压缩(去空格、缩变量名) | 深度优化(作用域折叠、Tree Shaking、死代码消除) |
| Tree Shaking | 一般 | 更激进 |
| 变量合并 | 基本合并 | 常量折叠+变量内联 |
| 代码混淆 | 否 | 支持 |
| 适用场景 | 开发环境 & 速度优先 | 生产环境 & 体积优化优先 |
如果你的项目 构建速度是瓶颈,继续使用 esbuild;如果 最终代码体积更重要,建议切换到 terser 进行压缩优化。
