Tree Shaking 排错关键在于构建前的静态可判定性:默认导出+属性访问、命名空间导入无访问、解构导入未使用项三类写法会导致整模块残留,须改用具名导出/导入并验证产物。

Tree Shaking 排错的关键不在运行时,而在构建前的导入导出关系是否“静态可判定”。只要引用路径模糊、导出结构不扁平、副作用未声明,摇树就会失效——哪怕你只调用了一个函数。
识别三类高发失效模式
以下写法在代码里看起来很合理,却会让 Tree Shaking “失明”:
-
默认导出 + 属性访问:如
import utils from './utils'; utils.formatDate();。整个utils对象(含未调用的formatPhone、randomId)都会被保留 -
命名空间导入但无实际属性访问:如
import * as utils from './utils'; console.log('init');。部分构建工具会因无法确认是否通过字符串访问(utils[xxx])而保守保留全部导出 -
解构导入却未使用全部项:如
import { debounce, throttle } from './utils'; debounce(fn, 300);。虽然现代 Rollup/Vite 多数能剔除throttle,但若utils.js被其他地方以非具名方式引入过,它仍可能整体存活
验证是否真被摇掉,别靠猜
直觉容易误判,必须看产物:
- 打开构建后文件(如
dist/assets/index.xxxx.js),全局搜索目标函数名(如subtract)。若存在且未被压缩混淆,说明未被移除 - 在入口加一行测试导入:
import { unusedHelper } from './utils';,重新构建并对比打包体积。体积没变 → 已生效;体积增大 → 该导出此前未被剔除,需回溯原因 - 用
webpack-bundle-analyzer或vite build --report查看模块明细,确认utils.js中哪些导出进入了最终 chunk
修复对象访问类残留的核心动作
本质是让“怎么引”和“怎么用”完全对齐:
- 把
export default { methodA, methodB }改为独立具名导出:export function methodA() {}、export function methodB() {} - 严格使用具名导入:
import { methodA } from './utils',禁用import utils from和import * as utils - 第三方库优先选 ESM 版本,例如用
lodash-es替代lodash;同时检查 Babel 配置,确保未启用@babel/plugin-transform-modules-commonjs - 在
package.json中正确声明sideEffects:若工具函数纯计算无 DOM 操作或全局修改,设为"sideEffects": false;若有 CSS 或字体等副作用资源,精确列出路径(如"./src/styles/*.css")
绕不开的 CommonJS 坑
即使项目用 ES Module,一旦依赖中混入 CommonJS 模块,Tree Shaking 就基本失效:
- 像
const { add } = require('./math');这类写法,导出是动态对象,构建工具无法静态分析内部结构 - 解决办法:推动上游库发布 ESM 版本;或本地封装一层 ESM wrapper,如新建
math-esm.js,里面用import()动态加载再重新导出 - 检查 node_modules 中关键依赖的
package.json,确认其exports字段是否指向.mjs或type: "module"兼容入口


















