⚡️ 万字总结 前端构建引擎 Rspack 前世今生与高性能内幕

794 阅读15分钟

本文参考 Rspack 官方文档/成员社区分享和本人的一些理解,如有不同观点,欢迎评论

Why Rspack?

前端工具现状

开发/生产构建时间长

  • 巨型应用
  • 开发:启动时间过长,通常为 5-10 分钟,单次 HMR 10-20 秒
  • 生产:构建时间过长,通常为 10-20 分钟,降低持续部署的效率

配置不灵活

  • 公司业务种类繁多,需要支持各种开发场景
  • 大多数开发工具无法同时满足"高构建性能"与"优秀的配置"活性"
  • 新生代构建工具生态不够成熟,部分场景无法开箱即用

产物性能不达标

  • 产物的性能直接影响了用户体验
  • 大多数开发工具无法同时满足"高构建性能"与"优秀的生产环境优化能力"

开发者的诉求

  • 冷启动+HMR 性能:冷启动要快,生产构建也要快
  • 灵活性:构建工具的配置要足够灵活,能应对各种使用场景
  • 生产环境产物性能(极致的拆包能力):**Code Splitting **等能力决定了产物性能。C 端收入对产物性能非常敏感,直接影响业务指标(首屏性能)
  • 生产环境构建性能和稳定的产物质量
  • 丰富的生态
  • 丰富的应用场景:webapp,nodejs,跨端应用
  • 迁移成本:最小化,Legacy code 兼容

社区解决方案

改造 webpack

  • 性能优化解决方案 thread-loader,swc-loader,esbuild-loader,cache-loader,persistent-cache、hard-source-webpack-plugin、lazy compilation、DIIPlugin MFSU
  • 然而,这些解决方案的切入点比较单一,并没有解决根本问题。比如 swc-loader 只能在 loader 层做优化,但是 webpack 有其他性能瓶颈;vite 生产环境构建性能差,拆包能力;esbuild 不支持 hmr,打包能力,插件能力弱很多

总结:治标不治本,性能提升没多少,配置复杂翻倍

换框架

Parcel

架构很好,但是用的太少

Turbopack

架构很好(Rspack 和 Turbopack 大概同时诞生),和 nextjs/Vercel 公司绑定

Rollup

库场景良好,应用场景性能和功能都缺失的较为严重

esbuild

缺陷

  • HMR:Hook 不支持增量构建,rebuild 性能较差;esbuild 原生不支持 HMR,也没有提供接口,所以需要自行通过插件实现,性能下降严重
  • 产物性能不佳
    • 只支持基于 dynamic import 和多 entry(multi entry)的拆包
    • 无法控制包的体积和并行加载数目
    • esbuild 只能编译到 es6,所以对于 Es5 支持会引起很大的性能劣化,且和 splitting 配合不友好
    • 很容易拆分出非常多小的 chunk,网络加载和离线化场景性能很差
    • 自定义的后处理会导致 chunk hash 问题

esbuild 带来的启发

  • 验证了基于原生语言架构的 bundler 的性能天花板非常高
  • resolve 和 load hook 基于 golang regex 的设计**巧妙避免了跨语言通信开销,**一旦正则不匹配,就不会调用 callback 了,避免了每一次调用都触发 callback(go 和 js 之间的反复调用会存在性能问题)
  • 验证了没有一套基于增量架构的 HMR 是不可行的(没有 hmr,bundler 再快也没用)
  • 验证了增量构建中跨语言通信是瓶颈问题

vite

中小型项目很香,开发体验很好;但是 Build 性能虽然一般(rollup),对中小型项目足够

缺陷

dev 性能:

  • 瀑布流问题导致 reload load 的性能瓶颈
  • HMR 并不总是能工作
  • Fast-Refresh 的 bailout 情况非常多,大型项目非常容易触发 bailout
  • Legacy 代码大量循环引用(bailout),改造兼容成本很大

Build 性能:

  • vite 生产环境使用 rollup 编译【和 webpack 一样单线程】,且不支持 cache,导致性能不佳
  • Build 性能真的重要吗?CI 越长,迭代效率越低

Build Time Matters

  • CI 可以做更多的事情,预览、包分析、E2E 等
  • 减小快速试错的成本(想想改个配置编译 30min)
  • Cl Build 成本(如 aws 的 codebuild,省钱才是硬道理)
  • 减小 HotFix 的时间(热修变冷修)

产物性能:

  • 拆包能力比 esbuild 好(支持 manualChunks 和 minChunkSize 可以控制包大小,手动分类 module)
  • 拆包能力仍然是导致其难以应用到重要 C 端应用的最大阻力(广告 C 端对性能及其敏感)(缺少像 webpack 的控制包大小,并发网络请求数量)
  • 重要的 C 端应用通常需要拆包的调优来获得最佳的性能
已知问题

vite 的选择

vite 的经验
  • 出色的开箱即用体验非常重要(why we build rsbuild)(webpack 弱项)
  • 兼容生态十分重要,避免所有轮子都要自己造一遍
  • 高质量的 loader 和 plugin 里包含了无数的细节,小团队根本无去维护
  • 社区至关重要
  • bundleless 目前不适合公司的大型业务场景
  • 拆包能力至关重要,关乎到能不能应用到重要 C 端应用(webpack 强项)

why rust

  • 生态丰富(swc):做 bunder 的底层依赖很多,如 transform、parse、minifiy,swc 暴露了 rust crates 调用能力
  • 语言工程化做得好 cargo 工具链完爆 js 工具链
  • 良好的 binding 支持(NAPI-RS)

第一次尝试:Rusty Esbuild

Rusty Esbuild + HMR + Rollup Plugin API = rolldown

为什么不扩展 rollup 呢?

答案:rollup 不适合做应用构建-核心架构问题

  • Rollup 的架构是为了解决库的打包问题,只有 esm 为一等公民,其他均需要转换成 esm 进行处理
  • commonjs 的支持是错误的方案,不可能彻底实现兼容(non strict cjs ->strict esm),除非引入 runtime(esbuild)
  • 没考虑其他资源的多样性,如 css、图片、esm、commonjs 等语言在 resolve 层面的差异性

Rollup 对 commonjs 的处理也是 vite 开发生产不一致的一大根源

kinsta.com/blog/rollup…

medium.com/webpack/web…

@rollup/plugin-commonjs 做过无数重构,目前还是有很多 end case

非严格模式和严格模式的运行时语义完全不一样

为什么要 rust 版本的 esbuild?

介绍下 esbuild:Esbuild 是由 Figma 的 CTO 「Evan Wallace」基于 Golang 开发的一款打包工具,相比传统的打包工具,主打性能优势,在构建速度上可以比传统工具快 10~100 倍。那么,它是如何达到这样超高的构建性能的呢?主要原因可以概括为 4 点。

  1. 使用 Golang 开发,构建逻辑代码直接被编译为原生机器码,而不用像 JS 一样先代码解析为字节码,然后转换为机器码,大大节省了程序运行时间。
  2. 多核并行。内部打包算法充分利用多核 CPU 优势,所有的步骤尽可能并行,这也是得益于 Go 当中多线程共享内存的优势。
  3. 从零造轮子。 几乎没有使用任何第三方库,所有逻辑自己编写,大到 AST 解析,小到字符串的操作,保证极致的代码性能。
  4. 高效的内存利用。Esbuild 中从头到尾尽可能地复用一份 AST 节点数据,而不用像 JS 打包工具中频繁地解析和传递 AST 数据(如 string -> TS -> JS -> string),造成内存的大量浪费。

缺点

  • 架构比 rollup 更适合应用,css、图片等均作为一等公民,resolve 和 commonjs 相比都更加正确(commonjs 引入 runtime 处理保证其语义是正确的)
  • 插件 api 极其薄弱 onLoad onResolve,难以支持复杂需求,几乎无生态(n 个转换处理怎么建模,没有 transform hook)
  • content-hash 问题在 esbuild 上几乎无解(鸡蛋问题)
  • 内容哈希的生成:通常,在构建过程中,构建工具会根据文件的内容生成一个哈希值,并将这个哈希值嵌入到文件名中。这样,文件内容每次变化时,生成的哈希也会变化,从而触发浏览器的缓存更新。
  • esbuild 的优化和构建模式:esbuild 是一个非常快速的构建工具,但在实现上可能没有像 Webpack 那样的完善哈希管理机制。例如,WebPack 会通过 contenthashchunkhash 在文件名中嵌入内容哈希,来确保文件内容发生变化时浏览器能够重新加载资源。而 esbuild 在这方面的支持相对较弱,特别是在处理多文件构建时,可能无法像 Webpack 那样优雅地生成和控制每个文件的哈希。
  • 鸡蛋问题:这里的“鸡蛋问题”可以理解为一个悖论或矛盾。简单来说,想要在构建过程中控制哈希值,首先需要确保文件内容的变化能直接影响哈希值的生成,但 esbuild 作为一个快速构建工具,往往通过优化性能而牺牲了这类细节处理。这就造成了在某些情境下,哈希值的变化并没有按预期反映文件的内容变化,或者哈希值的管理机制在多文件、复杂项目中没有 Webpack 那样成熟。
  • HMR 必须内置支持,外置插件实现无任何性能保障
为什么放弃 Rusty Esbuild?
  • esbuild 的 Native 插件和 JS 插件难以穿插组合

插件组合:假设你有一个模块需要一次经过三次转换处理,其中间一步转换可以原生 rust 实现,前后两步只有 js 版本实现,要怎么处理?

webpack loader 设计天然适合 rust 工具链的渐进式迁移:Loader 的组合性可以实现工具链的渐进式原生化。

  • esbuild 和 rollup 在生产环境优化上做的太基本,bundle splitting 和 tree shaking 两个深度优化在原有架构实现风险较大
  • 海量业务都是基于 Webpack 的,迁移成本非常大
为什么选择 Rusty webpack?
  • 产物的性能和质量都有充分保证
  • 迁移成本低,可以实现渐进式迁移(双引擎切换)
  • loader 的设计非常适合组合 Rust 的 loader 和 js 的 loader
  • 架构设计上支持 AST 的复用
  • language agnostic 的设计使得应用侧的扩展非常灵活
  • 产物不依赖 ESM,使得其可以完美适配不支持 ESM 的环境,如 Lynx、Miniapp 等场景

Rspack 特性

Rspack 是基于 Rust 的高性能 Web 构建引擎

  • 基于 Rust 实现,内置增量编译机制(webpack 没有),HMR/构建速度极快,无论项目多大,hmr 的耗时基本一致,目前还没达到完全常数化水平,随着项目变大,影响比较小
  • 针对 webpack 的架构和生态进行兼容,无需从头搭建你的生态
  • 提供 TS、TSX、JSX、CSS、CSS Modules、Sass 等开箱即用的支持
  • 默认内置多种优化策略,如 Tree Shaking、CodeSplitting、代码压缩等等

生态兼容性

Loader

babel-loader

sass-loader

less-loader

style-loader

css-loader

@svgr/webpack

postcss-loader

raw-loader

url-loader

file-loader

vue-loader/svelte-loader

svelte-loader

@mdx-js/loader

@svgr/webpack

image-webpack-loader

thread-loader

source-map-loader

node-loader

...

plugin

webpack-bundle-analyzer

mini-css-extract-plugin

terser-webpack-plugin

react-refresh-webpack-plugin

html-webpack-plugin

define-plugin

copy-webpack-plugin

progress-plugin

webpack-stats-plugin

html-webpack-plugin => @rspack/plugin-html or builtins.html

react-refresh-webpack-plugin => builtins.react.refresh

webpack.DefinePlugin=> builtins.define

webpack.ProvidePlugin=> builtins.provide

mini-css-extract-plugin=>experiments.css

tsconfig-paths-webpack-plugin=> resolve.tsconfigPath

copy-webpack-plugin=> builtins.copy/copy-webpack-plugin@5

webpack-bundle-analyzer

webpack-stats-plugin

fork-ts-checker-webpack-plugin

...

前端框架

  • vue

x.com/youyuxi/sta…

www.rspack.dev/zh/guide/te…

vue 生态兼容:TS 语法处理:Rspack 内置后处理;Less 语法处理:less-loader->Rspack 内置后处理

  • react

www.rspack.dev/zh/guide/te…

Rspack 原生支持了 JSX,TSX;Dev 下内置支持 ReactFastRefresh

  • svelte

从 Webpack 迁移

开箱即用

TypeScript 是 Rspack 中的一等公民,我们提供了开箱即用的能力力,零配置

CSS、CSS Modules 是 Rspack 中的一等公民,我们提供了开箱即用的能力,零配置

你仍需要配置 less-loader、sass-loader 对非 CSS 的资源进行 transform

Less 需要由 less-loader 进行转译,可以直接配合 Rspack 内置的 CSS 后处理逻辑

转译后的 CSS 使用 Rspack 内置的 CSS、CSS Modules 后处理器完成处理

迁移准则

  • 优先使用内置功能
  • SWC>babel-loader,Rspack 使用 SWC 编译 JavaScript 代码。如果非要使用 babel-loader,也尽量控制在比较小的影响范围内
  • experiments.css > style-loader + css-loader, Rspack 使用 SWC 实现了 experiments.css,默认开启
  • 资源模块 Asset Modules > file-loader + url-loader + raw-loader
  • html 生成:builtins.html > @rspack/plugin-html

性能收益

benchmarks:github.com/web-infra-d…

分析:Rspack 不是最快,旨在做到足够灵活,生产环境产物足够优的,够快即可,指标最均衡

架构设计

  • 核心架构脱胎于 Webpack5:架构稳定性高
  • 拥抱 Native 语言的高并发架构:将原本在 JS 里难以并行化的操作充分并行化
  • 从语言特性上,rust 编译器做了非常多的优化
  • js 在 v8 优化上已经不错了,最大的短板在于多线程的支持,目前 js 多线程基本是基于 buffer 或者 string 通信,所以导致很多复杂的数据结构通信比较困难,比如 ast,只能做序列化通信,一旦涉及到序列化和反序列化,会导致序列化和反序列化本身会有很大的开销,意味着虽然用了多线程,但是序列化和反序列化的开销可能就会抵消多线程带来的收益。但是rust 多线程共享数据结构非常简单,基本没有任何开销,多线程收益明显。所以 rspack 在线程数越多收益越高
  • webpack 内部做了很多优化,比如针对 v8 做了很多,但是为啥慢呢?webpack生态太慢!因为大部分生态是基于插件和 loader 扩展的,babel-loader
  • 基于 Rust 的 Babel 替代品 SWC(parser,transformer,minify)
  • 基于 napi-rs 的 Rust 和 JS 的高效通信桥接

为什么插件扩展通过 js?

  • rust 学习成本;
  • 难以满足业务侧灵活多变的需求;
  • js 动态化的特性。rust 作为 native 语言,做动态化远不如 js,涉及到比较大的问题就是即使用 rust 开发了一个插件,插件编译产物(native code)和操作系统/cpu 强相关,所以想做 native 插件的动态分发、加载比较困难,需要熟悉整套扩平台编译的知识,门槛高,稳定性不保障。

更快的 webpack

不是抛弃 webpack,而是一个更快的 webpack

1. 高度LTO代码;2. 高并发度

为什么跑 jsloader 耗时:js 是单线程的,其他的任务会被这一个 jsloader 阻塞,跑不了其他 module 的 analyze

基本和 webpack 架构一致,并且对 webpack 能优化的部分进行了优化

  • make 阶段:生成模块依赖图
  • seal 阶段:会将模块图生成 chunk,和生产环境关联非常大
    • code spliting:基于多入口或者 dynamic import(路由懒加载)做code spliting按需加载
    • bundle spliting:支持业务侧灵活拆包,控制包大小,缓存三方依赖
    • 优化 optimize analytics:比如 tree-shaking
    • 代码生成:根据不同的 runtime 环境&用户配置,通过 chunk 生成产物,不同宿主环境加载逻辑不一样(node 环境 require 加载其他 chunk;浏览器根据 jsonp 加载其他 chunk)

Rspack 速度快的核心原因:rspack 会尽可能的进行并行化,比如模块生成(根据 id 解析路径->加载模块内容->parse->递归把依赖加到构建过程中,每个模块都会经过这个过程,比如一个模块有 10 个依赖,这 10 个依赖可以并行做加载。webpack 做不到核心原因是他单线程的设计,或者可能通过 worker thread 做到,但是模块信息是非常复杂的数据结构,所以带来的性能收益不明显),模块分析优化 optimization analysis 等...

增量编译 HMR

传统 webpack 构建是重新构建 ModuleGraph,Rspack 不会完全重构建,比如在第 N 次的 compliation 会在 change phase(会映射到多个 module),将多个 module 重新 rebuild 一遍,patch 到 ModuleGraph 上就可以了,最后完成 seal phase 和 emit assets。整体的 ModuleGraph 是可以被下一次 compliation 继续复用

社区和展望

Rspack stack

image.png

根据官方成员描述,未来还会有RsStack test框架

image.png

「❤️ 感谢大家」

如果你觉得这篇内容对你挺有有帮助的话: 点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。觉得不错的话,也可以阅读 Sunny 近期梳理的文章(感谢掘友的鼓励与支持 🌹🌹🌹):

我的博客:

Github:https://github.com/sunny-117/

前端八股文题库:sunny-117.github.io/blog/

前端面试手写题库:github.com/Sunny-117/j…

手写前端库源码教程:sunny-117.github.io/mini-anythi…

热门文章

专栏