Tree Shaking 是什么?为什么有时不生效?
简化版
Tree Shaking 是构建器通过静态模块图标记未使用导出,再由生成和压缩阶段安全移除不可达代码的优化。它依赖 ESM 等可静态分析结构;CommonJS、顶层副作用、错误的 sideEffects、动态访问、转译后丢失 ESM,以及没有执行生产优化,都会使代码无法删除或被误删。
详细版
ESM 的 import/export 顶层结构让构建器在不执行代码时知道依赖关系。Tree Shaking 不是简单搜索变量名,而是分析导出是否被使用、模块是否必须求值、某条语句是否可能有副作用。
Webpack 中 usedExports 标记导出使用情况,sideEffects 提示整个模块或文件能否跳过,两者层级不同。错误写成 sideEffects: false 可能把 CSS、Polyfill 或注册代码一起删掉。
应保持库的 ESM 输出,正确标注副作用,在生产模式构建,并用产物分析和最小消费用例验证。是否“按需导入”不能只看源码写法,最终 bundle 才是证据。
完整版教学
一、为什么静态模块结构是前提
构建器必须在不运行程序的情况下回答“谁导入了谁、用了哪个导出”。ESM 的静态导入导出位于顶层,名称关系清晰,适合形成模块图。
import { add } from './math.js'
console.log(add(1, 2)) // subtract 未被引用
CommonJS 可根据条件拼接路径或修改 module.exports,分析器往往只能保守保留。不是所有 CommonJS 都完全不能优化,而是动态性让可靠证明更困难。
记忆钩子:Tree Shaking 只删除构建器能证明“删掉也不会改变可观察行为”的代码。
二、从标记使用到真正删除经历哪些阶段
构建器先建立模块图,标记入口可达模块和已使用导出,再判断模块求值是否有副作用。最终代码生成与 minifier 才可能把无用声明和表达式移除。
解析 ESM → 建依赖图 → 标记 used exports
↓
判断模块/语句副作用 → 生成代码 → 压缩删除
因此开发构建中仍看到未使用代码,不代表生产 Tree Shaking 失败。开发模式常保留结构以提高构建速度和调试体验,必须检查实际生产产物。
三、sideEffects 与 usedExports 为什么不是一回事
usedExports 关注模块内部哪些导出被使用,删除语句时还要让压缩器判断副作用。sideEffects 是包或文件级提示:若模块无导出被用,能否连模块及其子树的求值一起跳过。
| 层级 | 典型配置/分析 | 能解决什么 |
|---|---|---|
| 模块图 | sideEffects | 跳过无副作用文件及子树 |
| 导出 | usedExports | 标记未使用导出 |
| 语句 | /*#__PURE__*/、压缩分析 | 删除无副作用调用结果 |
把 sideEffects: false 理解成“我的包没有副作用”是一项作者承诺。若承诺错误,优化器按提示删除代码不是构建器 bug。
四、哪些代码属于不可随意删除的副作用
修改全局对象、注册事件、执行 Polyfill、写存储和引入全局 CSS 都可能在导入时产生可观察行为。即使模块的任何导出都没被引用,导入本身仍可能必须保留。
{
"sideEffects": ["**/*.css", "./src/register.js", "./src/polyfills.js"]
}
若组件库错误设置全包 sideEffects: false,消费者只导入 Button 时,Button 的 CSS 可能被当成可删除模块,最终组件有 DOM 却没有样式。正确做法是将 CSS 和初始化文件列入副作用白名单。
五、为什么看似纯函数仍可能留在 bundle
JavaScript 的 getter、代理、动态属性和函数调用都可能有副作用。表达式 factory()(Component) 是否会写全局、抛错或触发 getter,压缩器无法凭名字知道,只能保守保留。
/*#__PURE__*/ 可提示某次调用结果在未使用时可删,但这同样是开发者承诺,标错会改变行为。它只作用于特定表达式,不会自动把整个模块都声明为无副作用。
假设库原始 300 KB,使用 20% 导出却因初始化副作用保留 240 KB;正确拆分副作用后产物降到 90 KB,减少 150/240=62.5%。真实项目必须通过构建报告测量,不能从导入数量反推体积。
六、系统排查不生效或误删除
先确认生产优化已开启,再看消费端解析到 ESM 还是 CommonJS 入口,接着查看包的 sideEffects 和转译配置。若 Babel 把 ESM 提前转为 CommonJS,后续 bundler 会失去最有价值的静态信息。
| 现象 | 优先检查 | 验证方法 |
|---|---|---|
| 整个库进入产物 | 入口格式、动态引用 | 查看模块来源 |
| 未用函数仍存在 | 顶层调用、副作用 | 检查压缩前后代码 |
| CSS 意外消失 | sideEffects | 最小消费应用构建 |
| 开发大、生产小 | 构建模式 | 比较生产报告 |
对库作者而言,最可靠测试是建立只导入一个组件的 fixture,构建后验证只包含必要 JS、样式和初始化行为。Bundle analyzer 展示“有什么”,运行测试证明“删后仍正确”,两者缺一不可。
七、常见误区与追问
- 误区:Tree Shaking 就是删除没有被 import 的文件。 它还要分析导出使用和副作用,粒度可到模块与语句。
- 误区:设置
sideEffects: false只会让包更小。 标注错误会让 CSS、Polyfill 或注册逻辑被误删。 - 误区:命名导入就保证只打包一个函数。 包入口格式、重导出和副作用仍会影响最终结果。
- 追问:为什么 ESM 更适合 Tree Shaking? 它的顶层静态绑定允许构建期建立明确依赖与导出关系。
- 追问:CommonJS 是否绝对不能优化? 不是绝对,但动态 require 和导出赋值让可靠静态分析更受限。
- 追问:
/*#__PURE__*/有什么作用? 它提示特定调用无副作用,未使用结果时可供压缩器删除。 - 追问:如何证明 Tree Shaking 生效且没有误删? 结合生产 bundle 分析、最小消费用例和运行测试验证。
八、加强记忆
把机制记成“静态建图、标记使用、判断副作用、压缩删除”:ESM 提供可分析结构,usedExports 管导出,sideEffects 管模块求值,PURE 注释辅助语句级证明。排查时既要问为什么没删,也要防止作者错误承诺导致删错,最终以生产产物和运行结果为准。