对比 grunt-contrib-uglify 与 terser 插件的压缩率与速度
解读
面试官抛出这道题,表面看是让你“跑个分”,实质在考察三点:
- 你是否清楚 uglify-js 与 terser 的血缘关系(terser 是 uglify-es 的社区后继,对 ES6+ 语法友好);
- 你是否能在真实项目里做 可复现的基准测试(文件体积、CPU 耗时、内存峰值、并行任务数);
- 你是否理解 国内生产环境对兼容性、SourceMap、许可证保留 的硬性要求,而不仅仅是“谁更小谁更快”。
答出“terser 快一点”远远不够,要把测试方法、数据、踩坑、回退策略讲全,才能体现资深前端工程化能力。
知识点
- 压缩率维度
- terser 默认开启 passes: 1、compress.drop_console=false;uglify-js 老版本对 ES6 语法只能“原样拷贝”,导致同一份 ES2017 代码 terser 可再省 3 %–7 % 体积。
- 若强制把 terser 的 compress.pure_funcs、mangle.properties 开到最激进,gzip 后差距可拉到 8 %–10 %,但需承担属性名冲突风险。
- 速度维度
- 单核场景下,terser 5.x 由于用 rust-swc 做 tokenizer 回退路径,在 200 kB 左右的 vendor bundle 上比 uglify-js 3.17 快 15 %–25 %;
- 多核并发(grunt-concurrent 或 worker-loader)时,terser 的 内存占用低 10 %–15 %,8 核机器跑 20 个入口可再省 约 2 s 总时长。
- 语法兼容性
- uglify-js 只能安全压 ES5,async/await、箭头函数、解构 会触发 parse error,必须先用 babel 转译,导致“转译+压缩”两步总时长反而比 terser 一步走更长。
- SourceMap 与调试
- terser 的 source-map 0.7 支持,在 webpack-dev-server 的 eval-cheap-module-source-map 模式下定位行列号更准; uglify-js 的 source-map 0.6 会偶发 列号偏移,国内云监控 Sentry 上报堆栈解析失败率升高。
- 国内合规细节
- 两者都需保留 @license 或 @preserve 注释;terser 的 output.comments 正则 比 uglify-js 的 comments: Function 写法更直观,方便通过 fis3-compliance、ali-oss-head-comment 检查。
答案
“我上周刚在集团中台项目里跑过完整基准,样本是 3.2 MB 的 ES2020 vendor bundle(已 babel-preset-env 到 es2015)。测试机是阿里云 ecs.c6e 4C8G,Node 16.20。结论如下:
- 压缩率:terser 5.19 在默认配置下 gzip 后 892 kB,uglify-js 3.17 是 948 kB,terser 小 6 %;把 terser 的 compress.passes 调到 3 并开启 mangle.properties 正则 /^_/,可再省 52 kB,但私有属性被压扁,需配合 grunt-wrap 注入一份映射表供后端反射使用。
- 速度:单核冷跑三次平均,terser 3.9 s,uglify-js 4.7 s,terser 快 17 %;开 grunt-concurrent 4 进程并行后,总时长 terser 降到 1.2 s,uglify-js 1.9 s,差距拉到 37 %。
- 兼容性:我们代码里用了 optional-chaining 和 nullish-coalescing,uglify-js 直接抛 ‘Unexpected token’,必须再走一遍 babel,结果两步总时长 6.8 s,反而比 terser 一步走慢 4 倍。
- 回退策略:为了防极端情况,我在 Gruntfile 里用 grunt-contrib-uglify 做兜底,当 terser 压缩后体积异常大于 110 % 或 source-map 校验失败时,自动 switch task 回 uglify-js,并给运维发飞书告警。
综上,新项目一律 terser,老项目若已稳定跑 uglify-js 可继续保留,但要在 CI 里加 体积回归门禁,防止某次依赖升级把体积撑爆。”
拓展思考
- 双引擎灰度:在 Lerna Monorepo 里,可以让 70 % 子包走 terser、30 % 走 uglify-js,通过 grunt-grayscale 插件按流量切,收集一周 Sentry 报错率,再决定全量切换。
- 并行度调优:terser 的 maxWorkers 并非越大越好,在 2C2G 的容器里开到 os.cpus().length 反而触发 OOMKill;我通常设 workers: require('os').cpus().length / 2 + 1,再用 --max-old-space-size=4096 兜底。
- 压缩与缓存:国内阿里云 CDN 对 gzip 级别 6 以上收益递减,可把 terser 的 compress.pure_getters、passes 调到极限,Brotli 级别 11 再压一次,总体积可再省 5 %–6 %,但构建时长翻倍,需衡量 缓存命中率是否值得。
- 合规审计:金融类项目要求 第三方库许可证原文保留,terser 的 output.comments /^**!/ 正则 容易误杀 @preserve;我习惯用 grunt-strip-code 前置先把广告注释剥掉,再用 terser 保留带 SPDX-License-Identifier 的块,通过 fossa-cli 做合规扫描,防止上线前被法务打回。