hsiaosiyuan0/mole

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()

执行方式

  1. 假设模块内部最顶层的定义为:

    $TopmostDecs = \{ VarDecStmt, FunDecStmt, ImportDec, ExportDec, ExprStmt \}$

    这也是我们当前执行 DCE 的最小颗粒度(函数内部的 dead-code 暂不考虑)

  2. 对于模块内的 $TopmostDecs$ 我们有函数:

    $OwnedOf: f(x) = TopmostDecs_{x}, x \in TopmostDecs$

    可以得到给定 $x$ 所持有的 $TopmostDecs_{x}$ 元素,比如在 M1 中 $OwnedOf(A) = \{C, d\}$

  3. $Entry$ 为起点,遇到的模块导入记为 $Import(x, M)$ 进入下一步

  4. 调用模块的 $M.OwnedOf(x)$ 方法,得到 $x$ 持有的 $TopmostDecs$,记为 $TopmostDecs'$

  5. 对于 $TopmostDecs'$ 中的每个元素 $x'$,标记其为 $Alive$ 并继续调用 $OwnedOf$ 方法:

    $\forall x' \in TopmostDecs', MarkAlive(x'), TopmostDecs^{\prime\prime}=M.OwnedOf(x')$

    如果 $TopmostDecs^{\prime\prime}$ 不为空集合 $\emptyset$ 则对其中的元素继续第 4 步,否则进入下一步

  6. 如果导入的模块 $M$ 中还包含其他导入,则继续第 4 步,否则进入下一步

  7. 对所有模块的标记完成后,重新计算标记为 $Alive$ 的元素体积(源码字符大小):

    $A = \{isAlive(TopmostDec_0),isAlive(TopmostDec_1),...,isAlive(TopmostDec_{|A|}) \}$

    $Size_{DCE} = \sum_{x=0}^{|A|} Sizeof(x)$