Tree-shaking and Side-effects
Closed this issue · 0 comments
背景
目前很多第三方包以及打包工具都支持了 Tree-shaking,所以 mole 在分析的时候也必须支持这个特性,以便更加精确地计算某个包被导入后的体积
Tree-shaking
Tree-shaking 又称为 DCE(dead code elimination),下面简称 DCE
在进行 DCE 之前,我们需要先收集模块间的引用关系,下图为其一般形式:
图中包含的信息为:
- Entry 表示入口文件,M1 和 M2 分别表示两个不同的模块。这里的一个模块可以通俗地理解为一个 JS 文件
- 模块中大写字母表示模块导出的内容,小写字母表示模块私有内容(外部无法使用)
- Entry 中使用了 M1 导出的 A
- M1 中的 A 使用了 M2 导出的 C,以及内部的 d
- M2 中的 D 使用了其内部的 e
确定了模块引用关系后,第二遍我们就可以以 Entry 为入口,标记触达的内容:
我们通过绿色表示从 Entry 开始可以触达的内容,通过灰色表示可以被 DCE 的内容。有个例外就是红色标记的 D,它不可以被 DCE,因为我们不确定 e 是否有副作用(side-effects)
副作用就是除了计算以外还做了其他的事情,在 JS 中,出现副作用的可能性非常多,比如:
- e 内部可能设置了全局变量,而设置的内容可能会被其他模块使用
const a = b + c
,虽然看起来是计算,但 b 可能是一个全局对象上的 Getter
这样的动态性,导致很难在静态阶段分析语句是否包含副作用。从打包工具的角度,它的优化行为必须是保守式的(Conservative),也就是「拿不准的就不干」,因为干了就可能出错,出错就很难调试排查,反而降低了效率
针对 D 这样的问题,现在的打包工具要求开发者通过注释 /*#__PURE__*/
手动标记表达式无副作用:
export const D = /*#__PURE__*/ e()
执行方式
-
假设模块内部最顶层的定义为:
$TopmostDecs = \{ VarDecStmt, FunDecStmt, ImportDec, ExportDec, ExprStmt \}$ 这也是我们当前执行 DCE 的最小颗粒度(函数内部的 dead-code 暂不考虑)
-
对于模块内的
$TopmostDecs$ 我们有函数:$OwnedOf: f(x) = TopmostDecs_{x}, x \in TopmostDecs$ 可以得到给定
$x$ 所持有的$TopmostDecs_{x}$ 元素,比如在 M1 中$OwnedOf(A) = \{C, d\}$ -
以
$Entry$ 为起点,遇到的模块导入记为$Import(x, M)$ 进入下一步 -
调用模块的
$M.OwnedOf(x)$ 方法,得到$x$ 持有的$TopmostDecs$ ,记为$TopmostDecs'$ , -
对于
$TopmostDecs'$ 中的每个元素$x'$ ,标记其为$Alive$ 并继续调用$OwnedOf$ 方法:$\forall x' \in TopmostDecs', MarkAlive(x'), TopmostDecs^{\prime\prime}=M.OwnedOf(x')$ 如果
$TopmostDecs^{\prime\prime}$ 不为空集合$\emptyset$ 则对其中的元素继续第 4 步,否则进入下一步 -
如果导入的模块
$M$ 中还包含其他导入,则继续第 4 步,否则进入下一步 -
对所有模块的标记完成后,重新计算标记为
$Alive$ 的元素体积(源码字符大小):$A = \{isAlive(TopmostDec_0),isAlive(TopmostDec_1),...,isAlive(TopmostDec_{|A|}) \}$ $Size_{DCE} = \sum_{x=0}^{|A|} Sizeof(x)$