SWC 压缩配置,我替你们把 terser 换下来后体积到底差多少?
SWC 压缩配置,我替你们把 terser 换下来后体积到底差多少?
先说结论(但标题不写”结论先行”了,这算正经结论):SWC 做压缩不仅能省 80%+ 的时间,压缩率跟 terser 在 1% 以内差距,而且配置相当灵活,完全能当 terser 的生产级替代。
这半年我陆陆续续把三个项目的构建压缩从 terser 换成了 SWC,包括一个 SPA 应用、一个库项目、一个 SSR 的 Next.js 站点。中间踩了不少坑,也试了各种配置组合,今天一次性说清楚。
为啥要换?先看数据
先来组基准数据压压惊。
同一个 React SPA 项目(15 万行 TS,大约 2500 个模块),用不同压缩工具在 CI 上跑:
| 压缩工具 | 构建时间 | 产物体积(gzip) | 配置复杂度 |
|---|---|---|---|
| terser (webpack 5) | 47s | 286KB | 中等 |
| esbuild (Vite 默认) | 6s | 296KB | 简单 |
| SWC minify | 8s | 289KB | 灵活 |
terser 压缩是最彻底的,但构建占了将近一分钟。esbuild 快是真的快,但压缩率差一点。SWC 属于两者之间的甜点区:速度接近 esbuild,体积接近 terser。
另一个只跑压缩(不经过构建工具)的裸对比,直接对同一份打包后的 JS 文件跑压缩:
文件: vendor.chunk.js (1.2MB)
terser: 耗时 12.3s → 输出 312KB (gzip: 86KB)
SWC: 耗时 2.1s → 输出 318KB (gzip: 88KB)
esbuild: 耗时 0.8s → 输出 328KB (gzip: 91KB)
SWC 比 terser 快 6 倍,体积多了不到 2%。这个差距对于绝大多数项目来说完全可以接受。
基础配置:.swcrc 怎么写
SWC 的压缩功能在 @swc/core 里自带,不需要额外装包。关键配置在 .swcrc 的 jsc.minify 字段:
{
"minify": true,
"jsc": {
"minify": {
"compress": true,
"mangle": true
}
}
}
这个最简单,等效于 terser 默认参数。但如果你真的只写这三行,那跟直接用 esbuild 没区别。SWC 的压缩配置项跟 terser 高度对应——它照着 terser 的 API 抄了一遍,但用 Rust 重新实现了。
压缩配置深度解析
jsc.minify.compress 可以传一个对象,里面包含几十个开关。我把常用的挑出来说:
必开的几个
{
"jsc": {
"minify": {
"compress": {
"dead_code": true,
"drop_debugger": true,
"conditionals": true,
"booleans": true,
"unused": true,
"side_effects": true,
"join_vars": true,
"if_return": true,
"sequences": true,
"collapse_vars": true,
"reduce_vars": true
}
}
}
}
这几个全是默认 true,不用你操心。dead_code 会去掉永远不会执行的分支,unused 会删没用到的变量和函数,side_effects 标记纯函数做 DCE(Dead Code Elimination)。
说实话,就靠这些默认配置,正常项目已经能拿到 90% 的压缩效果了。
真正需要你手调的
drop_console – 删除 console 语句
{
"compress": {
"drop_console": true
}
}
这个争议很大。线上去掉 console 能省体积,但万一出问题了没法远程调试。我的做法是:只对 console.log 和 console.warn 下手,保留 console.error:
{
"compress": {
"pure_funcs": ["console.log", "console.warn", "console.debug"]
}
}
pure_funcs 会把指定函数调用当成无副作用的纯函数直接干掉,效果跟 drop_console 类似,但可以精确控制。
passes – 压缩多跑几轮
{
"compress": {
"passes": 2
}
}
默认是 0(不限次数),但设成 2 或 3 就够了。不是越多越好——多跑几轮确实能再压一点,但收益递减非常快。我实测过的结果:
passes: 0 → 输出 318KB
passes: 2 → 输出 315KB
passes: 5 → 输出 314KB
第 2 遍还能挤 3KB,第 5 遍才多 1KB,不划算。
keep_classnames / keep_fargs – 要不要保留原名
{
"compress": {
"keep_classnames": true,
"keep_fargs": false
}
}
如果你用了依赖类名做反射的场景(比如装饰器、DI 容器),必须把 keep_classnames 开成 true。keep_fargs 默认是 false,意味着没用的函数参数名会被缩短,但不会影响调用。
toplevel – 顶层作用域压缩
{
"compress": {
"toplevel": true
}
}
默认就是 true,会压缩顶层变量和函数名。但如果你在写一个库,导出变量名被改了消费者就引用不到了。写库项目建议关掉:
{
"compress": {
"toplevel": false
},
"mangle": {
"toplevel": false
}
}
变量混淆(Mangle)配置
mangle 就是把变量名变成短的 a、b、c 来省体积:
{
"mangle": {
"keep_classnames": true,
"keep_fnames": false,
"keep_private_props": true
}
}
有几个容易踩的坑:
mangle.props – 要不要混淆属性名
默认不混淆属性名。如果你大胆开了 mangle.props:
{
"mangle": {
"props": {
"regex": "^_"
}
}
}
可以把 _internalVar 这种私有命名规范的属性缩成 a。但是非常危险,因为你没法保证 API 返回的数据里没有同名属性。我建议只有确定没有外部依赖的纯前端项目才开。
keep_private_props – 保护私有字段
TypeScript 的 #privateField 语法和 TypeScript 的 private 关键字,开了 keep_private_props: true 能保证它们不被误混淆。
格式(Format)配置
jsc.minify.format 控制输出格式,最常用的是注释:
{
"format": {
"comments": "some",
"ascii_only": true,
"indent_level": 0
}
}
comments 可选 false(删除所有)或 "some"(保留 JSDoc 风格的 @license 等注释)。ascii_only: true 会把非 ASCII 字符转义,对国际化项目有用。
实战:Webpack 里集成 SWC 压缩
如果你还在用 webpack,替换 terser-webpack-plugin 很简单:
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
terserOptions: {
compress: {
drop_console: true,
passes: 2
},
mangle: true
}
})
]
}
};
注意是 TerserPlugin.swcMinify 不是 TerserPlugin.swc。terser-webpack-plugin 从 5.3.x 开始内建了对 SWC 的支持,不需要额外装 @swc/core,因为 terser-webpack-plugin 自己会带。
不过我建议显式装一下,确保版本你能控制:
npm i -D terser-webpack-plugin @swc/core
实战:在 CI 上做增量压缩
这是我自己项目里的 GitHub Actions 配置,对 pr 构建做压缩差异对比:
- name: Build with SWC minify
run: |
npx swc src -d dist --config-file .swcrc
echo "## Compression Result" >> $GITHUB_STEP_SUMMARY
echo "| File | Before | After | Ratio |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|-------|-------|" >> $GITHUB_STEP_SUMMARY
for f in dist/*.js; do
before=$(wc -c < "src/${f#dist/}")
after=$(wc -c < "$f")
ratio=$(echo "scale=2; $after / $before * 100" | bc)
echo "| $(basename $f) | ${before}B | ${after}B | ${ratio}% |" >> $GITHUB_STEP_SUMMARY
done
配合 GitHub Actions 的 Step Summary,每次 PR 都自动展示压缩比,防止有人引入了不该引入的包。
跟 terser 的配置对照表
这个表我写了留着以后自己查:
| 功能 | terser | SWC |
|---|---|---|
| 删除 console | compress.drop_console |
compress.drop_console |
| 删除 debugger | compress.drop_debugger |
默认 true |
| 多轮压缩 | compress.passes |
同 |
| 纯函数标记 | compress.pure_funcs |
同 |
| 变量混淆 | mangle |
同 |
| 保留类名 | keep_classnames |
同 |
| 顶层作用域 | toplevel |
同 |
| 输出注释 | output.comments |
format.comments |
| ES 版本 | ecma |
同(作用于 compress 内) |
基本就是平替,配置项名字几乎一样,迁移成本极低。
注意事项 & 踩坑记录
1. SWC 的假设比 terser 更激进
SWC 默认假设代码运行在严格模式下,TDZ(暂时性死区)错误会被忽略。如果你的代码里有:
console.log(a);
let a = 1;
terser 会保留顺序,SWC 会在某些情况下把 let 转成 var 并提升——运行时行为不一样。这就是为啥压完后要跑一遍测试。
2. BigInt 和 Number 混用会出事
const a = 1n + 1; // 运行时抛 TypeError
SWC 优化时会假设算术运算没有副作用,直接把这类表达式优化掉,导致运行时行为变化。如果代码里有 BigInt 混合运算,建议关掉部分压缩:
{
"compress": {
"unsafe_math": false
}
}
3. 压缩完记得跑单元测试
这个其实是废话,但每次迁移压缩工具都会在这上面翻车。我在 CI 上加了一步:
npm run build:minify
npm run test:minified # 对压缩产物跑测试
一定要测,别信我上面任何一句配置——你的项目有自己的脾气。
总结一句
SWC 压缩配置就是 terser 的 Rust 翻版,配置项一一对应,快 5-10 倍,体积差不到 1-2%。对于大多数项目来说,直接换没有心理负担。
如果真要纠结那 1% 的体积差距,那 SWC 不值得换。但如果你想要 80% 的构建时间节省——换就完了。
下回准备聊聊 Rolldown 的插件怎么写的,最近在研究它的插件系统,比 Rollup 的钩子多了一倍。
评论区
登录后可评论。












