参考链接
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
进行压缩优化。