← 返回题目列表

Tree Shaking 是什么?为什么有时不生效?

高频 中等 第 15 / 31 题 更新于 2026/07/28
前端工程化Tree ShakingESM构建优化

简化版

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 注释辅助语句级证明。排查时既要问为什么没删,也要防止作者错误承诺导致删错,最终以生产产物和运行结果为准。