Replies: 13 comments 14 replies
-
作者的分享全面的讲解了各个 bundler 的优缺点,也了解了更多 rspack 的初衷与设计的思考过程,学习了很多,十分感谢 ❤️ 未来是一个标准化 & 生产稳定 & 超级快速都同时满足的 bundler 的时代,webpack 只具备前两项,这是业务最看重的,所以 webpack 仍是大部分人的首选 |
Beta Was this translation helpful? Give feedback.
-
Rollup 定位上还是 Single Bundler,对于业务比如多个入口构建的性能和优化尤其的弱,看 Rspack 是支持 Multiple Compiler,而且是并发的场景,不知道处理重复模块是不是也是共享的 |
Beta Was this translation helpful? Give feedback.
-
有点不太理解为什么transform hook 会造成高频的 Rust 和 JS 的通信开销,我理解的 Esbuild 的 filter 或 类Webpack|Rspack 的 rules 其实也是会有通信开销的,如果按照 rollup plugin 的设计原则,filter 应该是放在 PluginOptions 。 |
Beta Was this translation helpful? Give feedback.
-
看来 Parcel 中确实有很多独到的优秀设计,不过感觉 Parcel 的用户体验比起 Webpack 要差一些... |
Beta Was this translation helpful? Give feedback.
-
想问下Tree Shaking的实现里,关于从一个函数 变量粒度获取所有依赖源码的能力,有没有可能变成一个独立模块?我看市面上好像都没有直接可以复用的方案… |
Beta Was this translation helpful? Give feedback.
-
看完不确定理解对不对:rspack 想做的事情是不是其实就是用 rs 来实现一套 webpack 架构(最好能复用webpack 生态的插件/loader)的、同时又通过自定义一些具体实现(例如缓存和 swc)来提升构建效率的工具 |
Beta Was this translation helpful? Give feedback.
-
感谢作者的分享,学习到了非常多的东西,十分感谢❤ 最近也在思考对于巨石应用的编译构建提效的一些方案,例如在一些跨团队的大型复杂应用(小程序)的协作上,一些业务因为一些原因需要通过 npm package 的形式来进行业务代码的开发和发布(分包应用),然后再由主包应用去安装这些分包,然后走统一的编译构建流程将主包和分包的源码进行统一的编译构建(和微前端的形式还不太一样,微前端不同的应用、技术栈都可以独立编译构建、部署,然后在运行时阶段去打通不同应用之间的交互和运行)。而在小程序的场景下,一个小程序是一个独立的应用,不同的分包应用都需要被集成起来统一的编译构建。随着业务的发展,主包代码和分包代码都会增加,编译构建耗时当然也是呈正相关的(不论编译构建的语言使用 Rust、go、js 等)。 对于一个业务应用而言,站在 Bundler 的工作流程这个角度来看,首先分析一个入口文件,然后找寻依赖(模块),构建依赖,然后对于依赖的依赖持续这个过程。从构建过程的开始到结束的这个流程实际上是个串行的过程,也就是前置依赖分析构建完后才能进入到后续依赖的编译构建。 那么对于 Bundler 而言是否可以有分布式构建的能力,将这种串联的编译构建关系可以进行拆解:我所构想的一个场景就是分包的业务代码可以提前编译构建,不管是产出最终代码or中间代码,随着每次分包发版都包含这部分的产物。那么在主应用统一开始全量的编译构建的过程当中,Bundler 可以去消费分包产出的最终代码or中间代码,这样可以提前将其他的分包编译构建处理一次,减少主包应用全量编译构建的耗时,那么整个构建过程有点类似并行的过程,分包可以独立构建,然后被插入到主应用构建当中。 这个场景是一种理想态,那么回到具体的业务场景当中比较棘手的问题就是对于依赖的处理,场景一:我们所知道的对于 Bundler 来说,例如 webpack 会将依赖2次以上的模块输出到一个 chunk 当中,但是在分包构建流程当中,对于 Bundler 而言肯定是缺少对于其他分包应用,主包应用的分析的,所以对于部分模块可能会冗余输出,又或者另外一个场景:对于整个应用而言,有些基础框架的依赖,这些肯定只需要被打包成一份输出到一个 chunk 当中,然后被不同应用引入,但是在分包应用独立构建的时候肯定还不能提前把这些公共代码输出(有点类似 dll),等等一些暂时还没想到的问题。 简单而言就是:Bundler 分布式构建可以将部分模块独立构建并产出最终代码or中间代码,同时还能参与到其他的构建流程当中,将串行的构建流程进行解耦。 这个问题是这几天简单思考和总结描述了下,没有经过什么实际的代码测试,看到字节有这么多同学深入研究前端工程相关的内容,也期望可以相互交流和探讨这种形式是否可行以及一些技术困难点。 |
Beta Was this translation helpful? Give feedback.
-
能否评价下rollup4的新parser设计,将swc AST转为特定buffer传入js侧后手动还原为acorn AST |
Beta Was this translation helpful? Give feedback.
-
你们难道不觉得需要一个集团 Infra Team 统一 Oncall 来处理各条产线问题的情况很奇怪吗? |
Beta Was this translation helpful? Give feedback.
-
构建诊断分析工具 web doctor 这个有开源吗 |
Beta Was this translation helpful? Give feedback.
-
hardfist 兄写的真好,这篇文章是常读常新,每次读过后都有新的收获和感受。 |
Beta Was this translation helpful? Give feedback.
-
Great work, 工程上的思考特别有价值,感谢分享 |
Beta Was this translation helpful? Give feedback.
-
English Version
Why We Build Rspack
在开发 Rspack 之前,我们已经尝试开发了 n 款构建工具和框架,并在实际的生产环境下重度使用了 Webpack、Vite、esbuild、rollup 等构建工具,对各个工具的优劣处和设计取舍深有体会。
先介绍下团队背景,我们是公司的前端公共 Infra Team,负责维护(过)公司的前端通用构建工具和框架(有一些是开源的,有一些并没有),包含:
我们会发现所有这些工具和框架的包含一个很复杂的部分就是底层构建工具,实际上我们日常Oncall 处理最多的用户问题也是关于构建的疑问。
作为公司内部的 Infra 团队,和开源社区的运维方式的差异主要体现在:
我们第一个大规模使用的构建工具就是 Webpack,包括目前开源的 Modern.js 仍然在重度使用 Webpack,Webpack 的最大优点就是扩展能力极强,能够支持我们几乎所有的构建场景,但是缺点也比较明显。
在 Webpack 上对性能进行缝缝补补也难以解决性能后,业务团队也尝试使用了 Vite,我们 Infra 团队也尝试接入了 Vite,采用的方案也是社区上较为流行的双引擎方案,即上层框架侧是统一的配置和插件,但是底层的引擎是可以在Vite 和 Webpack 进行切换的,这虽然解决了一部分问题,但是其实带来了更大的挑战
说到 Rollup,我们在两个场景下深度使用了 Rollup,库构建方案和早期的 Lynx 构建方案,这期间也暴露了很多问题
Rollup 的优点非常明显,产物格式极为干净,产物结果对 TreeShaking 非常友好,但是同时其缺点也很明显
幸运的是 Rollup 的上述两个缺点,在 esbuild 下都得到了很好的解决,esbuild 把 CommonJS 当做一等公民,因此对 CommonJS 有着比较完美的支持,同时esbuild的性能极为出色,因此目前 esbuild 几乎是 Rollup 的一个很好的替代品,至少在库构建这个场景 esbuild 相比 Rollup 更为合适,这也是 tsup(底层为 esbuild) 成为 tsdx(底层为 rollup) 的一个更好的替代品的原因。Module Tool 目前的底层也采用的是 esbuild。
谈到 esbuild 解决了 Rollup 的 CommonJS 和性能两个最大的问题,我们基于此曾尝试将 esbuild 不仅用于库构建领域,还应用到应用构建领域(事实上,Remix 目前底层仍然使用 esbuild 进行构建),这期间同样暴露了非常多的问题。
上述的这些问题,导致我们渐渐放弃了 esbuild 的方案,专向自研 Rust Bundler 的之路。
How We Build Rspack
我们自研 Rust Bundler 并不是一开始就选择走 Rust Webpack 这条路,实际上我们最开始的路径更加简单,解决 esbuild 的目前的一些问题,因此最开始的路径其实是 Rust 版本的 esbuild | rollup 加上内置 HMR、CommonJS 和 Bundle splitting 的支持,因为我们已经在 esbuild 的基础上封装了一套完整的框架,在生产环境也跑了一年了,我们并不想重新来过,而是希望接口和esbuild 兼容,同时解决掉 esbuild 的 HMR 和 bundle splitting 问题。实际上 Rspack 的 legacy 分支仍然保留了当初兼容 esbuild 接口的设计。
但随着我们继续深入做下去,完成了第一版基于 Rollup 架构的 Rust Bundler,并完成项目的 POC 后,暴露了很多的问题,这导致我们重新审视架构,最终导致转向了 Webpack 架构。
一等公民(Language Agnostic)
与很多人的直觉可能相违背的是,Webpack 和 Parcel一样都是 language agnostic,而 Rollup 则是只有 Javascript 才是一等公民。这可能也是 Webpack 5 最为人忽视的一点,Webpack 5 支持了更多的一等公民模块。
插件 API 的设计
Rust Bundler 自从立项开始,自始至终就有一个核心需求,那就是支持通过 JavaScript 写Plugin,这点始终未变,因为我们在业务支持中,深知业务的扩展性是非常必要的,一个业务不可扩展的 Bundler 是难以落地的,因此如何设计插件 API 就成了我们的一个核心问题。
当我们设计插件的时候考虑的两个核心问题就是性能和可组合性,我们发现 Rollup 的 API 并不能满足我们的需求。
Note
Simple API is useful for adoption but maybe hard for scaling.
模块转换
所有 Bundler 的插件都要考虑的一个核心功能就是如何处理模块的转换,所有的 Bundler 都提供了插件支持该功能,但是不同插件的支持方式不同(rollup 对应的是 transform 而 webpack 对应的是 loader)。
我们综合分析了模块转换的功能,实际上发现这实际上是三个维度的需求
我们以 svgr 这个插件为例,来说明模块转换逻辑的复杂之处,svgr 的插件的作用是将一个 svg 文件转换为一个 React 的组件。我们来提炼下这里的三个要素:
在 Rollup 中实际将这三个维度揉进了一个 transform hook 里,这导致了两个比较严重的问题
我们调研了几乎市面上所有关于模块转换的插件 API web-infra-dev/rspack#315 ,最终发现只有 Parcel 和 Webpack 的 API 能够很好的解决该问题。
Parcel
Webpack
@svgr/webpack
loaderAST复用
另一个对性能影响很大的设计就是如何在不同的模块转换之间复用AST,因为 parse 的开销通常非常巨大且常常成为性能的瓶颈,如果能尽可能的复用 AST 则可以大大的优化性能。我们看看各个工具是如何处理 AST的复用的。
Esbuild
esbuild的处理方式最为直接,不支持模块转换操作,因此就不存在AST的复用问题,esbuild的parse、transform和minify都是共享同一个AST的,这也是esbuild的性能远远快于其他所有bundler的一个重要原因,缺点很明显扩展性很差。
Rollup
rollup可以在 load 和 transform hook里返回AST,这里要求的AST是标准的 ESTree AST,如果返回了标准的ESTree AST那么内部的parse就可以复用返回的AST避免重复parse。
Note
rollup在复用AST的一大优势是rollup只支持JavaScript,这意味着只需要考虑标准ESTree AST一种数据结构即可,但是这对于 Parcel和 Webpack却不适用。
Parcel
Parcel的transform plugin显然是经过深思熟虑的,Reusing ASTs 里详细讲述了如何实现AST的复用,Rspack的早期版本也也借鉴了该设计。Parcel的设计里解决了 AST的复用的一大难题,即如何处理string transform和 AST transform交叉的场景,对于一般的 transformer 实际存在四种情况。即
string -> string
,string -> AST
,AST -> AST
,AST -> string
,如何处理这些transformer的交叉执行是个难题,但是 Parcel 很好的解决了该难题。Webpack
webpack同样支持在loader里返回AST 来支持AST的复用,但是 webpack这里存在几个限制导致这个功能在社区并没流行起来。
首先webpack里返回的AST只能是符合ESTree标准的AST,但是不幸的是,社区的各种JS转换的loader返回的基本都不是标准的ESTree AST,包括 babel-loader(babel/babel-loader#539) 和 swc-loader(https://github.com/webpack/webpack/issues/13425), 这也导致即使其返回了AST,也难以在 Webpack 里进行复用,另一个问题是 webpack 虽然内部 parser 支持多种AST(CSS AST和 JavaScript AST),但是 webpack 目前也只支持JavaScript的AST,这导致虽然 Webpack 支持这个功能很长时间,但是其实也没有大规模应用。
除了模块转换和AST复用的问题,我们还考量了很多插件设计的问题,如怎样减小 Rust 和 JS 的通信频率,如何在不同的转换器之间进行 AST 的复用,避免重复 Parse 开销,如何处理Virtual Module等等。最后综合考虑下来感觉 Webpack 的架构更适合我们开发 Rust Bundler 对于定制化和性能的诉求,因此后面决定了走向 Rust Webpack 的道路(没有使用 Parcel 的原因是因为团队中几乎没人有 Parcel 相关的应用经验,Webpack 是个风险系数更低的选项).
Webpack 之路的探索
虽然我们整体路线上决定采用了 Webpack 的架构,但是在实施过程中还是走了很多弯路,
一等公民支持
我们在早期开发过程中,一方面收到了 esbuild 的影响,另一方面则是当时的 loader 尚未完全支持,因此我们决定扩展了对一等公民的支持(如支持 jsnext、ts、tsx、jsx 等 js 扩展语言作为一等公民),这虽然帮助我们快速的在业务侧进行了落地,但是也导致了较多问题
因此在未来 Rspack 考虑放弃对 ts、tsx、jsx 等模块的一等公民支持,而让用户通过 swc-loader 来进行编译处理,这保证了一等公民的处理和 Webpack 对齐,同时保障核心层的稳定。
Codegen architecture
如早期的 codegen 的方案采用的是基于 AST 的 codegen 的方案,而 Webpack 本身则是采用基于 dependency 的 string replacement 的方案,codegen 的方案是性能更优(避免了重复的 parse 开销),但是导致和 webpack 的架构有了偏离,实际上 webpack 的 Runtime 和 treeshaking 等逻辑都强依赖 string replacement 的依赖的信息,另一方面 codegen 也导致了后续的 persistent cache 的实现增加了不少难度,因此我们在 0.3 版本完成了 codegen architecture 从 AST 到 string replacement 的迁移,并发现性能并没有发生太大的衰减。
TreeShaking
因为我们早期选择了 ast based codegen 的方案,因此 treeshaking 也借鉴 esbuild 选择了对 AST 更为友好的方案,但是后来也暴露了该方案的不少问题,如 treeshaking 对于 reexport 和 multi entry 的优化(esbuild 的 treeshaking 优化相比 webpack 仍然有不小的差距evanw/esbuild#2049 ,因此我们后续逐渐将 TreeShaking 的实现过度到 Webpack 的实现,以实现更深入的 TreeShaking 优化策略。
类似的问题仍然有很多,但是我们仍然会继续探索下去。
Beyond Webpack
虽然 Rspack 的定位是 Webpack 的 Drop in Replacement,但是我们也深知 Webpack 也有诸多局限,我们也会在未来和 Webpack 合作,一起提升 Webpack 生态的用户体验。
Out of Box Solution
Webpack 最令人诟病的一点应该就是开发体验较差,对新人不够友好,给一个项目从头配置Webpack 并非易事,相较于 Vite 提供的开销即用的体验,Webpack 的体验就差的很多,另一方面随着 creact-react-app 的弃坑,社区上对于 Webpack 的上层封装选择已经不多,我们深知开箱即用对于新用户的重要性,我们在未来也会和 Modern.js 一起优化这块的体验。
Diagnostics
正如前面所说,Webpack 对于普通用户过于黑盒,缺乏必要的调试工具,一方面我们会继续深度优化 Web Doctor 来提升 Rspack | Webpack 的调试体验,另一方面我们也会在 Rspack 里加上更多的调试信息,做到调试体验的开箱即用。
Optimization
虽然 Webpack 对于产物的优化已经算是同类的佼佼者,但是仍然存在着不少的优化空间,如 Webpack 的 bundle spliting | code splitting | tree shaking 都是模块粒度的,这限制了很多的优化方式,我们未来会探索基于 function 粒度的优化手段,来进一步优化产物的运行时性能。
Portable cache + Remote build cache
我们公司内部有着大量的巨石应用和 Monorepo,Webpack 目前缺乏对 portable cache 能力的支持导致其很难实现分布式的构建缓存共享,但是分布式构建缓存共享对于巨石应用和 monorepo 的提速至关重要,我们在未来会重点关注这块能力的建设。
Beta Was this translation helpful? Give feedback.
All reactions