你的 sideEffects 真的配对了吗?—— 深度拆解构建工具的 Tree-shaking 潜规则
🚀 省流助手(速通结论):
sideEffects是给宿主(用你包的项目)看的声明,不是给你自己构建减重用的。- 只要包里包含 CSS/样式、全局监听(
process.on)或修改全局变量,绝不能简单设为false。/* @__PURE__ */的意思是 “这行没用到请删掉” ,而不是 “不能删”。- Bundle 并不安全:即便你打包成了单文件,一旦声明了
false,宿主打包工具依然能从内部“抠掉”你的副作用代码。
一、 线上“失踪”案:谁偷走了我的初始化逻辑?
很多开发者都遇到过这种诡异场景:本地开发时一切正常的全局监听(如 process.on('exit'))或样式文件,发布成 npm 包被别人使用后,在生产环境竟然“失效”了。
检查代码,逻辑都在;检查产物,文件也引了。最后发现,根源竟然是你在 package.json 中随手写下的那行:
json
"sideEffects": false
请谨慎使用此类代码。
你以为是在帮宿主做性能优化,实际上你是在给自己的代码下“逐客令”。
二、 生效时刻:它是谁的“紧箍咒”?
误区: 认为在库里写了 sideEffects: false,自己执行 vite build 时包体积就会变小。
真相:它的真正战场是「宿主编译时刻」。
-
自身构建时:当你运行构建指令时,工具遵循作者意图。只要你在入口写了
import './effect.ts',这段代码就会物理存在于你的dist产物中。 - 宿主打包时:当其他项目安装了你的包,宿主工具(Vite/Webpack)会读取你的声明。如果你承诺了“无副作用”,一旦宿主没引用你该模块导出的变量,工具就会开启“外科手术”:即使你的单文件 Bundle 物理上包含了这段代码,工具也会在最终输出时将其精准剔除。
三、 穿透 Bundle 的“外科手术”
这是最隐蔽的陷阱。很多开发者认为:“我打包时已经把副作用合并进 index.js 了,宿主引用了 index.js 就安全了。”
错了。 现代打包工具具备 Module Concatenation(模块提升) 能力。它们能“看穿” Bundle 内部的结构。只要你声明了 false,它们有能力从一个大的文件块中只“抠”出用到的函数,而把剩下的(包括那段 import './effect.ts' 产生的内容)当作垃圾直接丢弃。
四、 微观博弈:/* @__PURE__ */ 到底在帮谁?
如果说 sideEffects 是文件级的“粗调”,那么 /* @__PURE__ */ 就是语句级的“微操”。
纠正一个常见误区: 它是标记“可以删”,而不是“不能删”。
假设你的工具库有一个文件导出了 100 个函数,宿主只用了其中 1 个。
-
如果没有标记:剩下的 99 个导出中,如果包含
export const config = init()这种函数执行,打包工具会因为不敢确定init()是否修改了全局变量而保守地保留这一行。 -
如果加上标记:你是在给工具发“免责声明”。工具看到
/* @__PURE__ */,发现没人用config,就会放心地把这一行代码从产物中抹除。
五、 避坑总结:白名单管理
为了不让代码被“误杀”,你不能在包含副作用的文件里写 false。最专业的做法是使用数组进行精准保护。
哪些文件必须进 sideEffects 数组?
-
样式文件:
*.css,*.scss。 -
环境初始化:修改
global或window的脚本。 -
进程监控:包含
process.on或interval的逻辑。
推荐配置:
json
{
"sideEffects": [
"**/*.css",
"./dist/_init/*.mjs"
]
}
请谨慎使用此类代码。
结语
Tree-shaking 是一场开发者与构建工具之间的博弈。工具的本质是“保守”的,而 sideEffects: false 是你交给工具的一把“激进”的剪刀。
在下一篇中,我们将深入探讨:如何通过工程架构设计,强制开发者在编写副作用代码时进行“决策”,从而构建一套永远不会被意外误删的“契约式”架构。