Terser压缩配置实战与常见坑点避坑指南
又踩坑了,Terser 把我的 console.log 全干掉了,但线上报错却找不到源
今天上线前做最后的 build 检查,发现一个诡异问题:本地开发一切正常,console.log 都在;但生产环境跑起来后,某些关键日志没了,更糟的是——用户反馈报错,我打开控制台看 stack trace,发现全是 at t.a、at n.e 这种鬼名字,连文件名都只剩 main.3f8a.js:1:12345。调试?根本没法调。
第一反应是:是不是 sourcemap 没打出来?赶紧检查 webpack.config.js —— sourcemap 选项开着,devtool 是 source-map,build 后也确实生成了 .map 文件,CDN 也正常返回了。那问题在哪?
后来试了下发现,把 mode: 'production' 改成 'development',日志回来了,错误堆栈也清晰了。瞬间锁定:是 Terser 在搞鬼。
这里我踩了个坑:一直以为 Terser 就是个「压缩 JS」的工具,顶多配配 compress 和 mangle,结果它默认会干三件事儿:删 console、删 debugger、混淆变量名。而且这仨是默认开的,根本不用你显式写配置。
我翻了下 node_modules/terser/package.json 里的默认 preset(就是 terser-webpack-plugin 用的那个),确认了:没错,drop_console: true 和 drop_debugger: true 是内置在 compress 里的默认行为。而 mangle 默认也开,所以变量全变成 a/b/c/d……
本来想直接关掉整个 compress,但不行 —— 压缩率掉太多,gzip 后体积涨了 120KB,CDN 流量成本不能这么玩。得精细控制。
折腾了半天发现,Terser 的配置其实是分层的:compress 控制死代码删除和逻辑简化,mangle 控制名字混淆,output 控制生成格式(比如是否保留注释)。真正影响日志和可调试性的,主要是 compress 里的几个子开关。
我试过这些方案:
- 只关
drop_console:日志回来了,但报错堆栈还是乱码(因为 mangle 还在) - 关掉整个
mangle:堆栈可读了,但包体积涨了 80KB,不接受 - 设
mangle: { reserved: ['console', 'log'] }:没用,reserved 是防重命名全局变量的,console.log 不是变量,是属性访问 - 用
keep_fnames: true+keep_classnames: true:对箭头函数和 class 内部方法无效,还是糊
最后搞定的方案是:保留 mangle,但给关键函数加 @__PURE__ 注释?不对,那是给 tree-shaking 用的。等等 —— 突然想起来,Terser 支持 compress.drop_console 可以传数组,指定只删哪些方法。
对!不是布尔值,是数组。官方文档里藏得挺深,在 Terser compress options 里写着:drop_console: ["log", "info", "warn"] 这样写才对。默认 true 就是删所有,但你可以白名单式地留几个。
不过我最终没选白名单,因为团队规范是:开发/测试环境保留全部 console,生产环境只留 error 和 warn —— 既方便排查线上问题,又不至于刷屏。
所以最终 webpack 配置长这样(用的是 Webpack 5 + terser-webpack-plugin v5):
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: ['log', 'info', 'debug', 'table', 'group', 'groupEnd', 'time', 'timeEnd', 'trace'],
drop_debugger: true,
pure_funcs: ['console.assert'],
},
mangle: {
// 保留特定函数名,方便错误定位
reserved: ['jQuery', '$', 'require', 'define', 'exports', '__webpack_require__'],
},
output: {
comments: false,
},
},
extractComments: false,
}),
],
},
};
注意几个细节:
drop_console数组里没写error和warn,所以它们会被保留pure_funcs: ['console.assert']是为了防止 Terser 把console.assert(false, 'xxx')当成无副作用语句直接删掉(虽然一般不会,但加了安心)mangle.reserved里列了 webpack 和 jQuery 相关的全局标识符,避免被重命名导致运行时报ReferenceErrorextractComments: false是怕它把 license 注释抽到单独文件,影响部署流程
改完重新 build,验证结果:
- ✅
console.error('API failed')和console.warn('fallback triggered')都还在 - ✅ 错误堆栈里能看到真实函数名(mangle 没动 class/method 名,只动了局部变量)
- ✅ gzip 后体积比全关 mangle 小 67KB,比默认配置大 3KB —— 可接受
- ⚠️ 小问题:某些匿名函数里定义的
const log = console.log还是被删了,因为 Terser 检测不到这是 console 调用。但我们团队禁用了这种写法,所以不影响
顺带一提,如果你用 Vite,配置更简单,直接在 vite.config.ts 里写:
export default defineConfig({
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: ['log', 'info', 'debug'],
},
},
},
});
Terser 本身还支持通过注释控制单行行为,比如:
// @__NOINLINE__
function apiCall() { /* ... */ }
// 这一行不会被删,即使 compress.drop_console=true
console.warn('retrying...'); // eslint-disable-line no-console
但我不推荐大面积用注释,维护成本高,且容易漏。统一配在构建工具里最稳。
原理上再啰嗦一句:Terser 的 drop_console 不是正则匹配字符串,而是 AST 层面识别 CallExpression 节点,且 callee 是 MemberExpression,object 是 Identifier(console),property 是 Identifier(log) 这类。所以像 const c = console; c.log() 就绕过了检测 —— 但这也说明它很严谨,不是简单字符串替换。
以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案(比如用自定义插件在 AST 层做更细粒度控制),欢迎评论区交流。另外,下次遇到类似问题,别急着骂 webpack —— 先 grep -r 'drop_console' node_modules/terser 看一眼源码,比查文档快多了 😅

暂无评论