Skip to content

Design flaws

1. Transform Hook

rollup 的钩子判断和执行逻辑均在 transform 钩子中,也就是说每一个钩子的 transform 钩子都会为每一个模块执行一遍。这对于原生 bundler 来说并不可取,若原生 bundler 也借鉴了该设计,那么会存在性能问题。原生 bundler 会频繁与 javascript 通讯并调用 javascripttransform 钩子,即使这个模块的处理逻辑不在这个插件的 transform 钩子中。原生 bundler 调用单线程的 javascript 是十分昂贵的性能开销,通信开销在上万个模块通信场景下将会十分巨大,尤其是在 HMR 下,这将更难以接受。

esbuild 为例,模块需要先经过 filter 正则逻辑的过滤后,允许的正则表达式语法是 go 的正则表达式引擎支持的语法。这与 javascript 略有不同,go 的正则表达式并不支持前瞻(look-ahead)、后顾(look-behind)和反向引用(backreference)。go 的正则表达式引擎设计目的是避免部分会导致 javascript 灾难性指数时间最坏情况的性能问题。若命中 filter 逻辑,则 go 会与 javascript 通信并回调 javascripttransform 钩子,性能开销低的同时避免了大量 go 端与 javascript 端的通信。

Bundler的设计取舍 一文中也提到 rspack 也认同了 esbuild 的插件与原生通讯设计。

2. Incremental Build

rollup 增量更新类似于 reactfiber 树的增量更新,自顶而下的进行检测,对于入口模块每次都会调用 resolveId,同时对于每一个模块都会执行 load 钩子。模块若被命中缓存,则复用该模块的 transform 钩子的转译产物,同时还会复用该模块所包含的所有依赖模块的 resolveId 结果。

有了上述缓存特性,与 watch 模式下的初次构建相比减少大量不相关模块的 resolveIdtransform 钩子的执行次数,仅对入口模块和变更模块执行 transform 钩子,加速了 watch 模式下的热更新构建速度。不过遗憾的是,每一个模块都需要执行 load 钩子,这在大量模块的场景下,是十分消耗性能的。

rollup issue 2182rollup 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 机制。

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (2619af4)