Terser压缩配置实战与常见坑点避坑指南

欧阳静依 安全 阅读 2,751
赞 18 收藏
二维码
手机扫码查看
反馈

又踩坑了,Terser 把我的 console.log 全干掉了,但线上报错却找不到源

今天上线前做最后的 build 检查,发现一个诡异问题:本地开发一切正常,console.log 都在;但生产环境跑起来后,某些关键日志没了,更糟的是——用户反馈报错,我打开控制台看 stack trace,发现全是 at t.aat n.e 这种鬼名字,连文件名都只剩 main.3f8a.js:1:12345。调试?根本没法调。

Terser压缩配置实战与常见坑点避坑指南

第一反应是:是不是 sourcemap 没打出来?赶紧检查 webpack.config.js —— sourcemap 选项开着,devtool 是 source-map,build 后也确实生成了 .map 文件,CDN 也正常返回了。那问题在哪?

后来试了下发现,把 mode: 'production' 改成 'development',日志回来了,错误堆栈也清晰了。瞬间锁定:是 Terser 在搞鬼。

这里我踩了个坑:一直以为 Terser 就是个「压缩 JS」的工具,顶多配配 compressmangle,结果它默认会干三件事儿:删 console、删 debugger、混淆变量名。而且这仨是默认开的,根本不用你显式写配置。

我翻了下 node_modules/terser/package.json 里的默认 preset(就是 terser-webpack-plugin 用的那个),确认了:没错,drop_console: truedrop_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,生产环境只留 errorwarn —— 既方便排查线上问题,又不至于刷屏。

所以最终 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 数组里没写 errorwarn,所以它们会被保留
  • pure_funcs: ['console.assert'] 是为了防止 Terser 把 console.assert(false, 'xxx') 当成无副作用语句直接删掉(虽然一般不会,但加了安心)
  • mangle.reserved 里列了 webpack 和 jQuery 相关的全局标识符,避免被重命名导致运行时报 ReferenceError
  • extractComments: 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 看一眼源码,比查文档快多了 😅

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论