1. esbuild
2. terser
terser
之所以比 esbuild
产生的打包体积更小,主要是因为 它提供了更高级的优化手段,包括 作用域折叠(Scope Hoisting)、变量提升、代码混淆、AST 级别优化、更多高级压缩策略,而 esbuild
的压缩主要是 简单的语法转换和删除无用空格/换行符,缺少深入的 AST 级别优化。
下面我们从 代码优化原理、构建方式、作用域分析、Tree Shaking 等方面进行深入分析。
terser
和 esbuild
都基于 AST(抽象语法树)进行代码优化,但 terser
在 AST 级别执行了更多高级优化,而 esbuild
主要做基本的 minify(缩小代码)。
假设我们有这样一段 JavaScript 代码:
function add(a, b) {
return a + b;
}
console.log(add(1, 2));
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}
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 结构。在 js_printer
处理阶段:
function add
结构(因为 esbuild
不是一个高级压缩工具)add(1,2)
直接计算成 3
)所以最终输出:
function add(a,b){return a+b}console.log(add(1,2));
console.log(3);
?相比 terser
,esbuild
不会执行高级优化,例如:
add(1, 2)
并替换成 console.log(3)
。add()
只在一个地方调用,它可以被展开成 console.log(1 + 2)
。add()
没有被调用,它会直接删除 add()
。terser
会进行这些优化:terser
发现 add(1,2)
这个调用是 纯函数(pure function),可以直接计算并替换为 console.log(3)
,去掉 add
函数。console.log(3);
关键优化点:
terser
直接 折叠函数调用结果(常量折叠),消除了 add()
这个函数,从而减少代码体积。作用域折叠可以 减少作用域的嵌套,将函数或变量合并到更紧凑的作用域中。
原始代码:
function outer() {
function inner() {
console.log('Hello');
}
return inner;
}
outer()();
function outer(){return function(){console.log("Hello")}}outer()();
outer
这个作用域。console.log("Hello");
terser
直接移除了 outer
,因为它的唯一作用是返回 inner
,可以省略掉。作用:
terser
会尽可能减少变量声明的数量,合并多个变量定义,从而减少代码大小。
原始代码:
let a = 1;
let b = 2;
let c = a + b;
console.log(c);
let a=1,b=2,c=a+b;console.log(c);
esbuild
只是合并了 let
语句,但没有进一步优化。console.log(3);
terser
发现 c
是常量,直接替换掉,省略了变量声明。terser
能够分析并消除计算结果为常量的代码,而 esbuild
没有类似的优化。
原始代码:
const a = 100;
const b = a * 2;
console.log(b);
const a=100,b=a*2;console.log(b);
console.log(200);
terser
发现 b
是常量,直接用 200
替换掉 b
,从而减少代码体积。terser
可以彻底删除不会被执行的代码,而 esbuild
只能删除一些最基本的未引用变量。
原始代码:
function test() {
if (false) {
console.log("This will never run");
}
}
test();
function test(){}test();
esbuild
只删除了 if
语句,但 test()
这个函数还在。terser
发现 test()
没有任何作用,直接删掉整个函数调用。Tree Shaking 主要是用于删除未使用的模块,terser
在这方面比 esbuild
更激进。
原始代码:
import { unusedFunc, usedFunc } from './module.js';
usedFunc();
import { usedFunc } from "./module.js"; usedFunc();
esbuild
只是去掉了 unusedFunc
的导入,但代码本身依然保留 import
语句。import { usedFunc } from "./module.js"; usedFunc();
terser
在与 rollup
结合时,可以进一步优化 整个 import
语句,如果 usedFunc
也可以被内联,则可能直接删除 import
。esbuild
的目标是快速打包,而不是极致的压缩,因此做了一些权衡:
相比之下,terser
作为一个专业的压缩器,使用了:
这些优化让 terser
产出的代码体积更小,但牺牲了构建速度。
特性 | esbuild | terser |
---|---|---|
构建速度 | 快(10-100 倍) | 慢 |
代码压缩 | 基本压缩(去空格、缩变量名) | 深度优化(作用域折叠、Tree Shaking、死代码消除) |
Tree Shaking | 一般 | 更激进 |
变量合并 | 基本合并 | 常量折叠+变量内联 |
代码混淆 | 否 | 支持 |
适用场景 | 开发环境 & 速度优先 | 生产环境 & 体积优化优先 |
如果你的项目 构建速度是瓶颈,继续使用 esbuild
;如果 最终代码体积更重要,建议切换到 terser
进行压缩优化。