你的 sideEffects 真的配对了吗?—— 深度拆解构建工具的 Tree-shaking 潜规则

67 阅读3分钟

🚀 省流助手(速通结论):

  1. sideEffects 是给宿主(用你包的项目)看的声明,不是给你自己构建减重用的。
  2. 只要包里包含 CSS/样式全局监听process.on)或修改全局变量绝不能简单设为 false
  3. /* @__PURE__ */ 的意思是  “这行没用到请删掉” ,而不是 “不能删”。
  4. Bundle 并不安全:即便你打包成了单文件,一旦声明了 false,宿主打包工具依然能从内部“抠掉”你的副作用代码。

一、 线上“失踪”案:谁偷走了我的初始化逻辑?

很多开发者都遇到过这种诡异场景:本地开发时一切正常的全局监听(如 process.on('exit'))或样式文件,发布成 npm 包被别人使用后,在生产环境竟然“失效”了。

检查代码,逻辑都在;检查产物,文件也引了。最后发现,根源竟然是你在 package.json 中随手写下的那行:

json

"sideEffects": false

请谨慎使用此类代码。

你以为是在帮宿主做性能优化,实际上你是在给自己的代码下“逐客令”。

二、 生效时刻:它是谁的“紧箍咒”?

误区:  认为在库里写了 sideEffects: false,自己执行 vite build 时包体积就会变小。

真相:它的真正战场是「宿主编译时刻」。

  1. 自身构建时:当你运行构建指令时,工具遵循作者意图。只要你在入口写了 import './effect.ts',这段代码就会物理存在于你的 dist 产物中。
  2. 宿主打包时:当其他项目安装了你的包,宿主工具(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 数组?

  1. 样式文件*.css*.scss
  2. 环境初始化:修改 global 或 window 的脚本。
  3. 进程监控:包含 process.on 或 interval 的逻辑。

推荐配置:

json

{
  "sideEffects": [
    "**/*.css",
    "./dist/_init/*.mjs"
  ]
}

请谨慎使用此类代码。

结语

Tree-shaking 是一场开发者与构建工具之间的博弈。工具的本质是“保守”的,而 sideEffects: false 是你交给工具的一把“激进”的剪刀。

在下一篇中,我们将深入探讨:如何通过工程架构设计,强制开发者在编写副作用代码时进行“决策”,从而构建一套永远不会被意外误删的“契约式”架构。