tree-shaking 通过静态分析 es 模块结构构建引用图,标记未被引用的导出为 dead code,并结合副作用声明决定是否删除,最终由压缩器(如 terser)彻底移除。

Tree-shaking 能安全移除 Dead Code,靠的不是运行时试探,而是编译阶段对代码结构的静态推断。它不执行代码,只“看”结构、关系和约束,再结合副作用判断,决定哪些导出可以放心删。
依赖 ES 模块的静态可分析性
ES6 的 import/export 是语法层面固定的、不可动态改变的。打包工具(如 Webpack、Rollup、Vite)在解析时能直接构建出模块间的引用图:谁导入了谁、导入了哪些具体成员、有没有重命名或解构。这种确定性是前提。
- ✅ 支持 shaking:
import { throttle } from 'lodash-es'→ 工具明确知道只用了throttle - ❌ 不支持 shaking:
const _ = require('lodash')或import * as _ from 'lodash'→ 成员不可静态枚举,整包保留
构建引用图 + 标记可达性
工具从入口文件开始,顺着 import 关系逐层遍历,生成一张“使用关系图”。所有被路径连通的导出(包括间接依赖的),标记为“活跃”;没被任何路径触达的,就进入待删队列。
- 比如
utils.js导出sum和sub,但只有sum被某个import显式引用 →sub被标记为 dead - 即使
sub在源码里调用了另一个函数,只要那个函数本身也没被引用,整条链都会被标记为不可达
副作用检查决定是否真删
标记完还不够。有些代码虽然没被引用,但执行它会产生外部影响(比如改全局变量、注册事件、插入样式、调用 console.log)。删掉会改变行为,必须保留。
- Webpack 默认假设 ESM 模块无副作用,但你可以在
package.json中显式声明:"sideEffects": false(全无副作用)或"sideEffects": ["*.css", "src/init.js"](仅这些有) - 如果某模块的入口文件里写了
document.body.innerHTML = '',又没在sideEffects中说明,工具可能误删 → 导致运行时报错
压缩阶段最终剥离
标记只是第一步。真正删除发生在后续压缩环节(如 Terser):
- Terser 接收已标记的 AST,把 dead 函数体、未引用的 export 声明、死变量赋值等整段剔除
- 同时做常量折叠、简化控制流,进一步缩小体积
- 这个过程发生在生成最终 bundle 之前,所以你看到的输出里根本不存在那些代码











