很多 Vite 项目的配置里都躺着一行 drop: ['console'],升到 8.x 以后则是 dropConsole: true,图的是线上控制台清净、包小一点。可写调试日志的时候,手很容易把正经逻辑塞进括号:
console.log('合计', (order.total = sumLines(order.lines)))
console.info('剩余库存', stock.shift())
这两行在生产包里会被整句删掉。头一句带走了那次赋值,order.total 停在初始值,模板渲染出来就是「¥undefined」;后面那句的 shift() 也没执行,库存数组比预期多出一项。开发环境通常看不出异样,因为 Vite 8 的 dropConsole 挂在构建阶段的压缩选项上,vite dev 根本不经过它。
参数为什么也一起没了
esbuild 文档的 Drop 一节里写着:console 这一项会删掉整个调用表达式,所有参数都算在内。这么设计是故意的,线上能省掉那些参数的计算开销。代价是其中带副作用的表达式也会跟着消失,代码行为随之改变。原文还专门警告 “Using this flag can introduce bugs into your code!”。Oxc 压缩器的 dropConsole 说明也是同一个意思,并注明它的行为和 Terser 的 drop_console 类似。
换到语法树上看就清楚了。console.log(a = f()) 是一个调用表达式,赋值只是挂在它下面的一个参数子节点。压缩器按被调用者的名字匹配到 console.*,把整棵子树摘掉,里面的参数自然也就没了。
getter 同理。console.log(cfg.rate) 会触发一次属性访问;如果 rate 的读取器里顺手做了惰性初始化,日志一删,那段准备工作就跟着跳过了,后面用到这份数据的地方就可能拿到空值。
pure 走的是另一条路
同一份 esbuild 文档的 Pure 一节讲的是另一种语义:被标记的调用,只要返回值没人用,就可以删。注意它只作用于调用本身,不覆盖参数,里面带副作用的部分压缩后照样保留。文档里的例子,document.createElement(elemName()) 压完只剩里层那个函数调用。
放到开头那行代码,换成 pure 以后大致会压成 order.total = sumLines(order.lines):打印没了,赋值还在。前提是返回值确实没人接,写成 const r = console.log(x) 这种,整个调用会原样留下。
Vite 8 的配置位置变了
10 月的 Vite 8.3 里,build.minify 默认值已经是 'oxc',顶层 esbuild 选项也标成了弃用。迁移指南里写明,原来的 esbuild.drop 要改用 build.rolldownOptions.output.minify.compress 下面的 dropConsole 这类选项。旧配置不改也不会报错。用 8.3.5 打一次包就能看到,console 调用原样留在产物里,默认的 Oxc 压缩器并不读这项配置。
想要「去掉打印、保留副作用」,Oxc 文档给的办法是 compress.treeshake.manualPureFunctions:
// vite.config.js,Vite 8,默认 Oxc 压缩
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rolldownOptions: {
output: {
minify: {
compress: {
treeshake: { manualPureFunctions: ['console'] },
},
},
},
},
},
})
写成 'console',log、info 这些方法都会被覆盖;只想处理其中几个,就把全名逐个列出来。
还停在 Vite 7 或更早版本的项目,默认压缩器是 esbuild。对应写法放在顶层配置里:
// vite.config.js,Vite 7,默认 esbuild 压缩
export default defineConfig({
esbuild: { pure: ['console.log', 'console.info'] },
})
改用 Terser 的话,对应项是 terserOptions.compress.pure_funcs。它的选项说明里,drop_console 写的是直接丢弃 console.* 调用,放进 Vite 里实际打一次包,参数同样会被连带删掉。
代码层面也该收一收
- 日志括号里只放读取,赋值、
push、shift、自增这类改状态的操作挪到单独一行。 - getter 保持纯读取,惰性初始化放进显式的
init(),或者挪到第一次真正用到数据的业务代码里。 - 老项目里可以用
rg "console\.\w+\(.*(=|\+\+|\.push|\.shift|\.pop)"粗筛一遍,误报会有,但能很快圈出要细看的几行。 - 上线前对生产产物搜一下关键赋值,或者让端到端用例跑一遍构建后的包,比肉眼审配置靠谱。
你们项目里是直接 drop,还是已经换成 pure 了?