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

image.png

napi.rs 会基于 rust 的定义,自动生成一个 d.ts 类型文件,然后 require 这个文件,你就可以基于类型文件直接调用 rust 里面实现的方法了。 这里只是简单说了下,具体里面的逻辑非常多,我也没看明白。

踩坑过程

rspack 已经内置了非常多的原生支持,比如 TypeScript 、React ,所以它的配置简单得多。我们和默认配置不一样的主要几个点,less-loader,这个直接使用即可。tsconfig-paths ,这个我参考文档加上了 tsconfig.json 配置路径即可,似乎很简单,然后项目挂了。

image.png

image.png

关键问题在于,我创建了一个最小的项目,而且使用了同样的配置,那个小的项目就没有问题。不过,这个本来就是为了试试 rust ,遇到问题更好,可以进去 debug 看看。 然后把 rspack 项目拉下来,然后参考开发文档,打了一个 debug 包,使用 debug 包继续调试。debug 包终于看到错误信息了,然后遇到问题直接进去 debug 排查一下就行了。 当然,真实过程还是挺折腾的,一开始 vscode 的 debug 工具有点问题,无法调试,然后使用 lldb 直接调试可以进去,但是里面很多变量无法打印出来。

\$ 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 不会执行了。