前言
在前端性能优化中,减小 JS 包体积是重中之重。Tree Shaking(摇树优化) 就像它的名字一样:通过摇晃代码这棵大树,让那些无用的“枯叶”(死代码)掉落。本文将带你揭秘 Rollup 实现 Tree Shaking 的底层原理。
一、 核心基石:为什么是基于ESM?
Tree Shaking 的实现并非偶然,它深度依赖于 ESM (ES Module) 规范。
- 静态分析:ESM 要求
import和export必须在代码顶层,不能出现在if块或函数内部。 - 编译时确定:这意味着 Rollup 不需要执行代码,只需扫描一遍源码,就能在编译阶段清晰地知道模块间的依赖关系。
- 对比 CommonJS:
require是动态加载的,只有运行到那一行才知道加载了什么,因此 CJS 无法进行彻底的 Tree Shaking。
二、Rollup Tree Shaking 实现原理:从扫描到删除的四步曲
Rollup 的“摇树”过程可以分为以下四个精密步骤:
1. 递归扫描与依赖图构建
从入口文件(如 main.js)开始,递归扫描所有 import/export 语句。Rollup 会记录:
- 每个模块导出了哪些变量/函数。
- 每个模块导入了哪些内容。
- 模块间的引用链路(A 引用了 B 的哪个具体成员)。
- 基于这些信息,Rollup 会构建出一个完整的模块依赖图,清晰呈现整个项目的代码引用链路。
步骤 2:标记活代码与死代码
在模块依赖图的基础上,Rollup 会从入口文件出发,反向追踪所有被引用的内容,标记出活代码和死代码:
- 首先标记出哪些导出项被外部(其他模块或入口文件)引用;
- 接着判断这些被引用的导出项,是否真的在代码中被使用(而非仅导入未使用),若被使用则标记为活代码,未被使用则标记为死代码。
步骤 3:AST 分析优化(补充细节)
在标记活代码的过程中,Rollup 会深入分析每个模块的 AST(抽象语法树) ,精准追踪变量、函数的定义和引用关系。这里有一个容易被忽略的点:
- 即使一个变量、函数在模块内定义了,但既没有被 export 导出,也没有在模块内部被引用,它依然会被判定为死代码,被 Tree Shaking 摇掉——也就是说,Tree Shaking 不仅会处理“导出未使用”的代码,也会清理模块内部“定义未使用”的冗余代码。
步骤 4:删除死代码,生成最终产物
最后,Rollup 会遍历所有模块,只保留标记为活代码的内容,直接删除所有死代码(未被引用的导出项、模块内部未使用的定义等),最终生成精简、无冗余的打包产物。
Rollup 的 Tree Shaking 是原生支持的,无需额外配置,打包时会自动执行上述流程,且输出的代码更接近手写风格,无多余的运行时代码,优化效果直观可见。
三、 实战:不同导出方式的“招魂”效果
Tree Shaking 的效果,很大程度上取决于代码的导出方式——只有静态导出才能被 Rollup 精准分析,动态导出则无法实现 Tree Shaking。以下是常见的导出方式对比:
| 导出方式 | 是否支持树摇 | 深度原因分析 |
|---|---|---|
export const a = 1 | 完美支持 | 静态导出,引用关系明确。 |
export function b() {} | 完美支持 | 未被调用时可被精准识别并删除。 |
export default { a:1 } | 不支持/效果差 | 默认导出是一个对象,工具难以判断你是否会动态访问对象的某个 Key。 |
export * from './x.js' | 支持 | 按需转发,只会转发那些被下游真正引用的成员。 |
四、 Tree Shaking 避坑指南
避坑点 1:CommonJS 模块会导致 Tree Shaking 罢工
-
如果项目中引入了使用
require/module.exports的第三方库,Tree Shaking 会直接失效。原因如下:-
CommonJS 模块是动态模块,require 可以接收变量(如 require(
./${name}.js)),导入导出关系只能在运行时确定,Rollup 无法在打包前进行静态分析,因此无法识别死代码,Tree Shaking 自然无法生效。
实战建议:优先使用支持 ESM 规范的第三方库(如 lodash-es 替代 lodash),避免在 ESM 项目中混用 CommonJS 模块。
-
避坑点 2:副作用代码会干扰 Tree Shaking
-
如果模块中存在“副作用代码”(即执行后会影响全局环境、修改外部变量、执行 DOM 操作等的代码,如顶层的
console.log、window.xxx = xxx),即使这些代码未被引用,Rollup 也会保守地保留它们,避免影响项目运行逻辑,从而导致部分死代码无法被摇掉。解决方案:如果确认模块无副作用,可在 package.json 中添加
"sideEffects": false,告诉 Rollup 该模块可安全删除未引用代码;若有部分文件有副作用(如 CSS 文件),可显式声明:"sideEffects": ["./src/style.css"]。
避坑点 3:动态访问会导致 Tree Shaking 失效
-
如果代码中存在动态访问导出项的情况(如
import * as utils from './utils.js'; utils[dynamicKey]()),Rollup 无法在静态分析阶段确定哪些导出项被使用,会保留整个模块的所有导出项,导致 Tree Shaking 失效。实战建议:尽量使用具名导入(import { func } from './utils.js'),避免动态访问导出项。
五、 总结
Rollup Tree Shaking 的核心是“基于 ESM 静态规范,通过静态分析识别并删除死代码”,其流程简洁高效,且原生支持无需额外配置。想要用好 Tree Shaking,关键记住 3 点:
- 坚持使用 ESM 规范(
import/export),避免混用 CommonJS; - 优先使用静态具名导出,避免默认导出对象、动态导出;
- 处理好副作用代码,必要时通过 package.json 的 sideEffects 字段声明。