rspack 使用体验过程
why
首先说下为什么要尝试迁移,主要有几个原因
webpack 使用非常慢
rust 学习和尝试
纯粹好奇,我的项目升级以后到底能快多少?
构建更快的方案,其实是目前前端卷的最多的一个方向。一方面,基于 rust 重写各种工具,似乎成为一种有趣的挑战,webpack 是非常复杂的工具,这是一个具有挑战性的任务,所以大家愿意尝试。另外也是,更快有这更好的开发体验,同时更快的部署速度,出现 bug 的时候,能够更快部署上线,这对于很大很复杂的项目会更加重要。
结论
从 rust pack 的官网,我们可以看到对比数据,大概是 10x 的速度提升。不过,在我们的项目中,我一开始测试,确实大概是有 10x 的提速,从 40s 减少到 4s 。不过后面 wukun 群里提出的那个问题 ts 类型检测的问题,后面开启异步类型校验后时间效果大概如下
webpack | rspack | |||
|---|---|---|---|---|
项目 | 启动耗时 | 更新耗时 | 启动耗时 | 更新耗时 |
datago | 16s | 800ms | 4s | 300ms |
flink | 40s | 800ms | 8s | 1800ms |
大部分情况,rspack 是有一定的提效,但是没有 10x 那么高,大概是 4-5 倍提速,但是在 flink 项目中,更新耗时更长了,这有点奇怪。 当然这是我们目前的使用数据,具体原因还可以继续探索。因为 rspack 官网的对比项目配置也是关闭了 ts 类型检查的,具体原因后面可以继续对比探索。
选择 rspack 的原因
历史
在很早之前,没有 webpack 的时代,我们是不需要打包工具的。当时的方案,通过 require.js sea.js 之类的工具来动态加载模块。线上通过 cdn 的自动把文件打包合并成一个文件,比如 a.cdn.com/?a.js,b.js,c.js这样就可以拿到 a b c 三个文件。 但是在此之前,webpack 的出现还是非常重要的。webpack 的诞生其实挺简单的,当时的谷歌有一个 Java 版本的构建工具 Google Web Toolkit ,有一个叫做 code split 的特性。然后 sokra 很喜欢,但是他不喜欢 Java,当时找到一个类似的构建工具,叫做 webmake ,这个可能想做 web 版本的 make 。 然后,sokra 尝试给这个工具增加了 code split 的支持,然后他发现基本上把这个工具重写了,所以 webpack 诞生了。具体细节可以看看早期的 sokra 的分享。
特色
我理解任何一种工具都需要有自己的特色所在,webpack 诞生于对于 code split 特性的实现。 webpack 之外,现在比较流行的是 vite ,vite 的方案其实和最早的方案类似的,只是随着浏览器的发展,现在主流浏览器都已经默认支持 module ,也就是可以直接 import 模块了。 虽然 webpack 已经流行很多年,但项目越来越大,webpack 越来越慢的问题遭到很多人不满。实际上,我们的项目还不算特别大吧,我知道有些项目启动要 2 分钟。flink 在我们的项目中已经算很慢的了,估计这和我们把很多公共依赖提出来有关,这导致 module 数量少了很多。 针对 webpack 慢的问题,主要有两种方案提出
基于新的语言,比如 rust、go 重写整套体系
vite
当然,重写整套体系成本很高,很多人会担心一些兼容问题。这一套方案下有非常多的工具,比较火的是
esbuild:基于 go 重写
turbopack:这应该是第一个基于 rust 的实现
turbopack 背后最核心的代码生成方案是 swc 实现的,swc 是 rust 版本的 babel 。在 turbopack 以后,rspack 诞生了,rspack 的特色在于,他基本兼容 webpack 的,大部分 webpack 都是可以在 rspack 上跑的。 可以说,rspack 算是 webpack 的 rust 版本,并且他底层的插件机制保留了 webpack 老的模式。rspack 的插件体系和 webpack 一样基于 tapable 实现。 所以,我感觉选择 rspack 迁移应该是会最简单,并且兼容性应该也是最好的。
实现
我们使用都是通过 rspack 这个命令行工具来执行的,然后是 npm 包 rspack/core ,主要做参数透传以及 webpack 生命周期兼容相关逻辑。最终通过 rspack/binding 这个包来把负责启动 rust 包,rust 最终会打包成一个 node 的 native module,就是一个动态链接库,这样 rust 所有代码打包成一个文件,可以直接通过 v8 加载到内存,然后可以调用其中的方法。这里是通过 napi.rs 来打包的,node 的 native module 发展也经历过非常多次迭代升级,最终到目前的 napi 模式,可以一个文件运行于多个不同的 node 版本了,细节可以参考这里的文章。
image.png
踩坑过程
rspack 已经内置了非常多的原生支持,比如 TypeScript 、React ,所以它的配置简单得多。我们和默认配置不一样的主要几个点,less-loader,这个直接使用即可。tsconfig-paths ,这个我参考文档加上了 tsconfig.json 配置路径即可,似乎很简单,然后项目挂了。
image.png
\$ rust-lldb -- node ../rspack/packages/rspack-cli/bin/rspack build最终,我直接把 rspack_code 依赖的那个包代码修改了,然后直接 print 出错误日志,最终折腾了很久终于发现是我配置错了,要用绝对地址。 后面调试的问题发现是 codelldb 的一个 bug ,不知道为啥一直没修复,通过删除一个文件好了。
rm ~/.vscode/extensions/vadimcn.vscode-lldb-1.10.0/lldb/bin/debugserver模块丢失
这个问题修复以后,项目可以启动了,启动耗时 4s ,感觉很激动。然后打开页面,额,直接挂了。一开始报错,然后进去调试发现是一个模块找不到了
import { AddDataModel, IProps } from './index';
export class EditDataModel extends AddDataModel {}好像所有 index 文件都找不到了。然后构建了一个最小的 demo 发现 rspack 解析路径的是会有一个 bug ,首先会从 paths 配置里面找文件,如果找到了就直接返回。也就是 index 和 ./index 是一样的,因为我们项目中有一个 types/index文件,然后所有的 ./index都指向了这个文件。 不过这个问题修复也很简单,直接改下 nodejs_resolver 就可以了。 但最终,我发现 nodejs_resolver 已经不维护了,他们有了新的方案 oxc_resolver ,然后可以开启新的 resolver ,然后这个问题也解决了。
装饰器失效
看起来构建都没有问题了,然后页面访问,现在头有了,其他还是空白的,还没有任何报错。仔细调试发现,在 store 里面 set 了以后,组件没有重新 render 。然后看到生成的代码是这样的
class RoleStore extends BaseStore {
@observable isLoaded = false;
@action.bound
setRole() {
this.isLoaded = true;
}
}
// 老的生成代码
class RoleStore {
constructor() {
this.isLoaded = false;
}
setRole() {
this.isLoaded = true;
}
}
__decorate([observable], RoleStore.prototype, "isLoaded", void 0);
__decorate([action.bound], RoleStore.prototype, "setRole", null);
// rspack 生成的代码
class RoleStore {
isLoaded = false;
setRole() {
this.isLoaded = true;
}
}
__decorate([observable], RoleStore.prototype, "isLoaded", void 0);
__decorate([action.bound], RoleStore.prototype, "setRole", null);这样导致 isLoader 的 observable 失效了,所以虽然更新成功了,但是 observable 失效了,render 过程没有调用依赖收集,所以 isLoader 更新的时候, render 不会执行了。