tree shaking 是构建时静态分析优化机制,依赖 esm 模块系统;只有静态 import/export 可被分析,commonjs 因运行时加载无法摇树;需避免 export default 对象、确保细粒度文件拆分、配置 sideeffects,并通过构建产物验证效果。

Tree Shaking 不是某个 API 或语法,而是一种构建时的静态分析优化机制,依赖模块系统(ESM)和打包工具协同工作。想真正掌握它,关键不是背概念,而是理解“什么条件下能摇掉、什么情况下摇不掉”,并能主动设计代码去配合它。
搞懂前提:为什么只有 ES 模块才能 Tree Shake
CommonJS(require)是运行时动态加载,打包器无法在构建前确定哪些导出被使用;而 ES 模块(import/export)是静态声明,所有导入导出关系在解析阶段就能确定。这是 Tree Shaking 的底层基础。
- 确保你的源码用的是 export { funcA, funcB } 和 import { funcA } from './utils',而不是 module.exports 或 export default { funcA, funcB }(后者会阻碍摇树)
- Webpack/Rollup/Vite 默认只对 ESM 形式做静态分析,哪怕你用了 Babel 转译,也要保留 import/export 语法原样输出(即 Babel 配置中不要转成 CommonJS)
验证是否生效:别只看文档,动手看结果
最直接的方式是生成生产环境构建产物,然后检查最终 JS 文件里有没有没用到的代码。
- 用 Webpack 构建后,打开 dist/main.js 搜索未使用的函数名(比如 math.js 中的 funcWang),如果搜不到,说明摇掉了
- Vite 项目可运行 vite build --report,生成 report.html 查看各模块引用关系与未使用导出
- Rollup 可加 treeshake: { moduleSideEffects: false } 并观察 warning 输出
常见“摇不掉”的坑及写法建议
很多代码看似按需引入,实际仍保留了整包——问题往往出在导出结构或副作用上。
- 避免 export default 一个对象:如 export default { formatDate, deepClone },这种写法会让打包器无法单独剔除其中某个方法
- 拆分文件粒度要细:像 tene 那样每个工具函数单独一个文件(debounce.js、throttle.js),再统一在 index.js 中 re-export,这样 import { debounce } from 'tene' 才真能只打进 debounce
- 标记无副作用:在 package.json 中加 "sideEffects": false(全局无副作用),或指定数组如 "sideEffects": ["*.css"],否则打包器为安全起见会保留整个模块
进阶:配合 Babel 插件做更细粒度控制
当第三方库不支持 ESM 或导出结构不合理时,可以用 Babel 插件在编译阶段改写 import 语句,实现“逻辑上的按需”。
- 例如 @babel/plugin-transform-modules-commonjs 配合 babel-plugin-import,把 import { Button } from 'antd' 自动转成 import Button from 'antd/lib/button'
- 自己写简单插件也不难:监听 ImportDeclaration 节点,根据 specifier 名称重写 source,适合内部组件库统一规范
- 注意:这类插件属于“模拟摇树”,不能替代真正的 ESM + Tree Shaking,但能补足生态短板











