Design flaws
1. Transform Hook
rollup
的钩子判断和执行逻辑均在 transform
钩子中,也就是说每一个钩子的 transform
钩子都会为每一个模块执行一遍。这对于原生 bundler
来说并不可取,若原生 bundler
也借鉴了该设计,那么会存在性能问题。原生 bundler
会频繁与 javascript
通讯并调用 javascript
的 transform
钩子,即使这个模块的处理逻辑不在这个插件的 transform
钩子中。原生 bundler
调用单线程的 javascript
是十分昂贵的性能开销,通信开销在上万个模块通信场景下将会十分巨大,尤其是在 HMR
下,这将更难以接受。
以 esbuild
为例,模块需要先经过 filter
正则逻辑的过滤后,允许的正则表达式语法是 go
的正则表达式引擎支持的语法。这与 javascript
略有不同,go
的正则表达式并不支持前瞻(look-ahead
)、后顾(look-behind
)和反向引用(backreference
)。go
的正则表达式引擎设计目的是避免部分会导致 javascript
灾难性指数时间最坏情况的性能问题。若命中 filter
逻辑,则 go
会与 javascript
通信并回调 javascript
的 transform
钩子,性能开销低的同时避免了大量 go
端与 javascript
端的通信。
在 Bundler的设计取舍
一文中也提到 rspack
也认同了 esbuild
的插件与原生通讯设计。
2. Incremental Build
rollup
增量更新类似于 react
的 fiber
树的增量更新,自顶而下的进行检测,对于入口模块每次都会调用 resolveId
,同时对于每一个模块都会执行 load
钩子。模块若被命中缓存,则复用该模块的 transform
钩子的转译产物,同时还会复用该模块所包含的所有依赖模块的 resolveId
结果。
有了上述缓存特性,与 watch
模式下的初次构建相比减少大量不相关模块的 resolveId
和 transform
钩子的执行次数,仅对入口模块和变更模块执行 transform
钩子,加速了 watch
模式下的热更新构建速度。不过遗憾的是,每一个模块都需要执行 load
钩子,这在大量模块的场景下,是十分消耗性能的。
从 rollup issue 2182、rollup issue 3728 中可以看到,rollup
目前对于硬盘空间上的持久性缓存(Persistent Cache
)还不支持,也就是说 rollup
目前只支持对于 watch
模式下的增量更新,而不支持再次冷启动时的增量更新。webpack
支持了 Persistent Cache
,这也是 webpack
在二次冷启动上胜过 rollup
的原因之一。
Vite Incremental Build
vite
现阶段也还未实现完整的 Persistent Cache
,仅对 预构建 产物支持了 Persistent Cache
,详情可见 feat: Persistent cache-Jul 5, 2021,原因可能与部分配置文件的变更会导致缓存失效有关,需要考虑得更加周全,同时还提供了两个缓存思路。
第一个思路是在 插件层面 而不是在 整个依赖图层面 实现缓存。这种方式可以让缓存的 粒度更细,更容易管理。
第二个思路是在服务器端预先转换所有请求,并实现一个类似
import-analysis
的功能。这个功能使用转换请求的哈希值作为查询参数,并利用浏览器的强缓存机制。这种方法需要在文件发生变化时(通过文件监视器实现),以及在服务器重启时(就像这个PR
中实现的那样)递归地使清除插件缓存失效。这类似于vite
中的ssrTransformation
机制。