最近在调研 Rspack 的 incremental 实现,很多其他编译器实现增量构建的资料中都有提到一篇论文:
Build Systems à la Carte: Theory and Practice
[1]
,所以抽空学习了下发现挺有意思的,和 bundler 也有一些相关性。本文会简单介绍这篇论文的内容,并尝试从 build system 的角度来概括 bundlers。
à la carte:菜单的法语。
本文为了方便描述,省略很多细节;论文中有专门一章的内容描述了 real world build system 会遇到的问题,本文为了方便描述省略了这部分内容,real world build system 会有很多实际工程上的问题,所以本文仅作介绍,提供一个新的角度来看待问题。
Build system
Build system 指的是自动化执行一系列可重复任务的软件系统,常见的有
Make
[2]
、
Shake
[3]
、
Bazel
[4]
,他们以源文件作为输入,根据任务描述文件(比如:makefile)执行任务,构建出可执行文件。
Early cutoff:Task 重新执行时 Output 没发生改变时,依赖该 Task 的 Tasks 能否停止执行,提前完成构建。
Make
make = topological modTimeRebuilder
Make 使用 makefile 来描述任务,这些任务间的依赖关系明确,属于静态依赖,也不支持循环依赖,所以 Make 使用了 topological scheduler 以拓扑顺序执行任务。
Make 的 Info 构建信息其实就是文件系统本身,文件系统会有文件修改时间,Make 通过文件修改时间来判断任务是否需要重新执行,如果文件的修改时间早于其依赖文件的修改时间,则说明该任务需要重新执行,Make 将文件修改时间当做 dirty bit,属于 dirty bit rebuilder 的一种。
Make 通过 modTimeRebuilder 实现 Minimality,跳过不需要执行的任务,但也因为 modTimeRebuilder 导致它没有实现 Early cutoff,因为任务重新执行后输出的新的文件,尽管内容没变,文件修改时间也是改变的,导致不能提前中断,从这里也可以看出,没有实现 Early cutoff 的执行的任务一定不是最少的,所以 Minimality 往往是相对的。
不同于传统的 pass-based bundler,turbopack 并没有强调从头到尾的一个个编译阶段(pass),而是更接近于 query-based,定义任务,通过 query 获取任务结果,尤其是在 Dev 环境下,比如编译一个以 html 为入口的 web 页面,turbopack 的逻辑是:
传统 pass-based bundler 的逻辑是:
相比于 pass-based bundler,turbopack 只会关注获取 query 结果所需要执行的这一部分任务,其他无关任务不会执行,尤其 Dev 环境下不会有完整的 ModuleGraph 和 ChunkGraph。在 Production 环境下还是会通过一些方式来聚合成完整的图,以对完整 ModuleGraph / ChunkGraph 进行全局优化。
虽然 Vite 本身并不会 Bundle,但 Vite 在 dev 时还是会对任务不断进行执行,符合 build system 的定义,Vite 并不会对多个模块进行打包,而是对单个模块进行编译,所以 Vite 的任务逻辑其实很简单,就是编译模块。Vite 是在浏览器对模块进行请求时才去编译模块,浏览器没命中缓存才会发起请求,发起请求的顺序就是模块 import 的顺序,也是由浏览器决定的,所以可以看出 Vite 利用浏览器 ESM 模块系统作为自己的一部分 build system,属于 suspending + vtRebuilder 的组合。
利用浏览器 ESM 模块系统虽然会让本身的实现简单很多,但浏览器 ESM 模块系统本身并不是以 build system 为目标来实现的,相比真正的 build system 会带来很多限制,比如:
从 build system 的角度来讲,affected-based incremental 其实就是在 pass-based bundler 原有的 build system 基础上,引入新的 Rebuilder,让各个阶段之间的任务能够通过收集到的变更相互感知,以此能够对后续阶段的任务做 Early cutoff,通过添加 Early cutoff 这一特性来让 Rspack 更加 Minimality。这种方式更接近 self-adjusting computation:
The fundamental idea is to track the control and data dependencies in a computation in such a way that changes to data can be propagated through the computation by identifying the affected pieces that depend on the changes and re-doing the affected pieces. ——
Self-Adjusting Computation
[15]