likes
comments
collection
share

[译] 前 Firefox 工程师迁移到 Rspack 的经验教训Rspack 是一个基于 Rust 的 webpack

作者站长头像
站长
· 阅读数 18

本文翻译自 "Lessons learned switching to Rspack",原作者 Brian Birtles 为前 Firefox 工程师,W3C Web Animations 工作组成员。

注意:本文发布时,Rspack 的最新版本是 v1.0.0-beta.4。随着 Rspack 接近 1.0 正式发布,一些细节可能会有所变化。

Rspack 是一个基于 Rust 的 webpack 替代品,承诺能够更快,并且还包括一些常见的便利功能。在首次尝试的一年之后,我终于完成了将我最大的两个 webpack 项目转换为 Rspack。

以下是我在过程中学到的一些经验教训。

为什么选择 Rspack?

首先,为什么要选择 Rspack?

对我来说,这是因为我已经在我的两个最大项目中使用 webpack,而与其他构建工具相比,Rspack 提供了一个相对简单且低风险的升级方案。这些项目的构建时间也足够慢,加速构建可以显著提高生产力,并降低 CI 成本。

那么,为什么不选择 Vite 呢?很多人已经从 webpack 切换到 Vite,并且非常喜欢它。而我之所以选择 Rspack 并用于这些特定的项目,理由如下:

  • 对于 webpack 用户来说,升级到 Rspack 是一个更加简单且低风险的路径。
  • 据我所知,Vite 的 on-demand 文件服务在 SSR(服务器端渲染)上下文中(如 Next.js 应用)表现出色,但我正在使用 webpack 构建一个 SPA 和一个 Web extension,这种情况下并不是很重要。
  • 我对 Vite 的体验并不理想
  • 我喜欢保持开发和生产构建的结果尽可能接近,这样我可以自信地测试即将发布的内容。Vite 在这方面较弱,它在开发模式使用 esbuild 和在生产构建基于 Rollup。实际上,我发现两者之间的差距太大了,以至于我放弃了在 Vite 的开发模式中实现一些功能,因为需要两次实现这些功能实在是太费劲了。
  • 据说 Rspack 比 Vite 更快,至少在我关心的领域是这样的。

Rspack 潜在的最大问题,是像 webpack 一样具有上手成本,这应该是 Rspack 团队在 Rspack 之上还推出了一个更易用的工具 —— Rsbuild 的原因。

一路上的经验教训

在 Rspack 的 迁移指南 中,已经涵盖了从迁移 webpack 的步骤。

因此以下是我所学到的,未在指南中提及、或是可能不明显的事项。

TypeScript

Rspack 使用 SWC 来转换 TypeScript,这意味着你不再需要 ts-loader。然而,在使用 Rspack 构建后,我注意到生成的 JS 文件比使用 webpack 加 ts-loader 时大得多。

经过排查后发现,SWC 为 async iterators 等功能生成了很多样板代码。在 webpack 构建时,ts-loader 读取了我的 tsconfig.json,其中指定了 "target": "es2020",因此这些代码没有被降级到更低的版本。

然而,SWC 默认将它们降低到 ES5。解决方法是在 Rspack 配置中指定 SWC 的 target 即可。

例如:

const config = {
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: {
          loader: 'builtin:swc-loader',
          options: {
            sourceMap: true,
            jsc: {
              parser: {
                syntax: 'typescript',
              },
              // 在这里添加 target
              target: 'es2022',
            },
          },
        },
        type: 'javascript/auto',
      },
    ],
  },
};

我相信通过使用 env.targets 设置指定浏览器版本也可以实现这一点。

然而,即便这样做了,Rspack 生成的 JS 资源仍然较大,但当 Rspack 1.0 alpha 默认启用 optimization.concatenateModules 后,产物的体积缩小到与 webpack 相差不超过 3%,有时甚至略小。

类型检查

使用 SWC 转译 TypeScript 的结果之一,是默认不再执行类型检查。

你需要在 tsconfig.json 中启用 isolatedModules,并确定何时、以及如何执行类型检查。

我选择使用官方文档推荐的 fork-ts-checker-webpack-plugin,但仔细想想,我不确定这是否真的必要。

假设你已经设置好编辑器来执行类型检查,并在 CI 中运行 tsc,也可能将其作为 pre-commit 钩子(例如,使用 tsc-files),那么你可能不需要在每次构建时都执行类型检查。

使用 webpack 时,我在开发过程中忽略了一些 TypeScript 错误,以免它们在重构时打断我,方法如下:

use: {
  loader: 'ts-loader',
  options: env.ignoreUnused
    ? {
        ignoreDiagnostics: [
          6133 /* <variable> is declared but its value is never read */,
          6192 /* All imports in import declaration are unused */,
        ],
      }
    : undefined,
}

在使用 Rspack 时,如果你使用 fork-ts-checker-webpack-plugin 进行类型检查,你可以在那里传入类似的选项:

new ForkTsCheckerWebpackPlugin({
  issue: {
    exclude: env.ignoreUnused
      ? [
          // <variable> is declared but its value is never read
          { code: 'TS6133' },
          // All imports in import declaration are unused
          { code: 'TS6192' },
        ]
      : [],
  },
}),

CSS

尽管 Rspack 的迁移指南提到了将 mini-css-extract-plugin 替换为 rspack.CssExtractRspackPlugin,但 CssExtractRspackPlugin 的文档中指出:

如果你的项目不依赖 css-loader,建议使用 Rspack 内置的 CSS 解决方案 experiments.css 以获得更好的性能。

因此,对于我的 Web 项目,我能够将我的 CSS 配置从以下内容:

// rspack.config.js
const config = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { loader: MiniCssExtractPlugin.loader },
          {
            loader: 'css-loader',
            options: {
              url: false,
              importLoaders: 1,
            },
          },
          {
            loader: 'postcss-loader',
          },
        ],
      },
      // ...
    ],
  },
  // ...
};

更新为:

const config = {
  experiments: {
    css: true,
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [{ loader: 'postcss-loader' }],
        type: 'css/auto',
      },
      // ...
    ],
  },
  // ...
};

然而,我注意到我的 CSS 资源比 webpack 生成的资源大约大了 11%。通过比较输出的代码,我发现 Rspack 的输出没有执行某些优化,比如将 rgba() 颜色转换为十六进制值。

默认情况下,Rspack 使用 Lightning CSS 来压缩 CSS 资源,事实证明,Lightning CSS 使用的默认设置非常保守。在将一些更新的值添加到 minimizerOptions.targets 属性后,Rspack 的输出比 webpack 的稍微小了一些:

  optimization: {
    // ...
    minimizer: [
      new rspack.SwcJsMinimizerRspackPlugin({
        // ...
      }),
      new rspack.LightningCssMinimizerRspackPlugin({
        minimizerOptions: {
+         targets: [
+           'last 2 Chrome versions',
+           'Firefox ESR',
+           'last 2 Safari versions',
+         ],
        },
      }),
    ],
  },

Service Workers

我的应用程序包含一个 Service Worker,并依赖谷歌的 Workbox 的 InjectManifest 插件来触发 Service Worker 资源的生成,并填充其 precache manifest 。不幸的是,当我迁移时,这个插件尚未被 Rspack 支持。

不过,Rspack 1.0.0-alpha.0 添加了对 Service Worker(以及各种类型的 worklets)的原生支持。

有一点小的古怪之处在于,你需要传递一个 URL 对象给 navigator.serviceWorker.register()(而不是字符串),并且你也不能传递一个引用 URL 对象的变量。

例如,以下代码会为 sw.ts 生成一个 Service Worker 的片段:

const registrationPromise = navigator.serviceWorker.register(
  new URL(
    /* webpackChunkName: "serviceworker" */
    './sw.ts',
    import.meta.url,
  ),
);

你还需要注意确保 Service Worker 文件最终拥有一个固定的文件名,而不是带有 hash 的防缓存文件名:

// rspack.config.js
const config = {
  // ...
  output: {
    chunkFilename: (assetInfo) => {
      if (assetInfo.chunk?.name === 'serviceworker') {
        return '[name].js';
      }
      return '[name].[contenthash].js';
    },
  },
};

然而,所有这些仍然无法像 InjectManifest 那样,嵌入一个 precache asset manifest。我尝试了各种替代方法,但都无法与 Rspack 原生的 Service Worker 支持一起工作,所以最终我通过提取 Workbox 的 InjectManifest 插件中所需的部分,并将其适应 Rspack 来编写自己的解决方案。

一段时间后,Rspack 宣布 Workbox 已完全支持。但是我写的包更小,并且在进行更改时似乎比 Workbox 更好,所以我暂时会坚持使用它,但我相信大多数人会乐意继续使用 Workbox。

然而,原生 Service Worker 支持有一个特点,那就是它似乎在消除无用代码之前运行。因此,如果你在构建时使用常量来禁用 Service Worker 注册,就像在下面的代码中一样,你可能会发现即使它没有被引用,Service Worker 资源仍会生成。

function registerServiceWorker() {
  if (!('serviceWorker' in navigator) || !__ENABLE_SW__) {
    return Promise.resolve(null);
  }

  // The following code will be eliminated when __ENABLE_SW__
  // is falsy, but the sw.js asset will still be generated.
  const registrationPromise = navigator.serviceWorker.register(
    new URL(
      /* webpackChunkName: "sw" */
      './sw.ts',
      import.meta.url,
    ),
  );

  // ...

  return registrationPromise;
}

React Cosmos

在迁移到 Rspack 时,最大的障碍可能是让 React Cosmos 与之协作。我是 React Cosmos 的忠实粉丝,因为我发现在开发和测试组件时,它不像 Storybook 那样需要很多额外的开发依赖,而是与现有的打包工具一起工作。

不幸的是,React Cosmos 不支持 Rspack,只支持 webpack、Vite 和其他一些工具。它提供了关于如何配置自定义打包工具的说明,我心想:"将 webpack 插件移植到 Rspack 有多难呢?"

事实证明,答案是 "相当难",这主要是因为我坚持用 tsup 构建插件,并且对每个 endpoint 应该如何打包感到困惑。

不过就在几周之后,react-cosmos-plugin-rspack 诞生了,为 webpack 插件提供了一种即插即用的替代品。

Knip

最后一个需要注意的依赖是 Knip。Knip 是一个用于检测项目中未使用冗余项(如依赖、导出、文件等)的工具。如果它检测到你在使用 webpack,它会查找你的 webpack 配置,检测项目的 entrypoints、webpack 插件等,并相应地分析你的项目。不幸的是,它并不支持 Rspack。

我的公司 是 Lars 对 Knip 工作的赞助商之一,所以我联系他看看是否可以委托开发一个用于 Rspack 的 Knip 插件。Lars 很快接受了,并邀请其他人参与众筹,几天后 插件就完成了

作为结果,Knip 检查出我不再需要以下任何依赖:

  • clean-webpack-plugin(已被 output.clean 替代)
  • copy-webpack-plugin(已被 rspack.CopyRspackPlugin 替代)
  • css-loader(已被 experiments.css 替代)
  • mini-css-extract-plugin(已被 experiments.css 替代)
  • react-cosmos-plugin-webpack(已被 react-cosmos-plugin-rspack 替代)
  • ts-loader(已被 builtin:swc-loaderfork-ts-checker-webpack-plugin 替代)
  • webpack, webpack-cli(已被 @rspack/cli 替代)
  • webpack-dev-server(内置于 @rspack/cli
  • workbox-webpack-plugin(已被 @birchill/inject-manifest-plugin 替代)

命令行选项

Rspack CLI 包含一个内置的开发服务器,但不幸的是,命令行选项在某些地方与 webpack-dev-server 不同,尤其是缺少像 --port--hot/--no-hot 这样的选项。

为了解决这些问题,我最终使用了各种 --env 值,并在 rspack.config.js 的配置函数中进行检查。这似乎是一个不必要的兼容,希望在 Rspack 未来的版本中得到解决。

Rsdoctor

Rspack 团队很贴心地创建了一个插件 Rsdoctor,用于分析你的包和构建时间。以下是我最终使用的配置:

new RsdoctorRspackPlugin({
  disableTOSUpload: true,
  linter: {
    rules: {
      // Don't warn about using non ES5 features
      'ecma-version-check': 'off',
    },
  },
  supports: { generateTileGraph: true },
});

没有改变的部分

除了上述变化之外,其他一切似乎都能如常工作,包括以下插件:

  • html-webpack-inject-preload
  • html-webpack-plugin
  • Relative CI webpack plugin
  • terser-webpack-plugin
  • web-ext-webpack-plugin
  • webpack-preprocessor
  • webpack-utf8-bom

我还使用了 Bugsnag webpack 插件,但它们只在新版本发布时运行,所以我还没机会进行测试。

结果

那么,Rspack 速度究竟有多快呢?

对于我的 SPA 项目,我得到以下结果:

环境webpackRspackRspack(不进行类型检查)
笔记本19.1s8.1s (-57.7%)3.04s (-84.16%)
台式机13.24s5.57s (-57.94%)2.44s (-81.61%)

最右边一列的结果是最重要的,因为类型检查是在一个单独的进程中进行的,大多数时候你只需等待构建完成,

此外,如果我正确理解 Rsdoctor 的分析结果,大部分编译时间似乎都来自 postcss-loader,因此我希望当 Tailwind 4 发布时,我可以切换到使用 Rspack 内置的 lightningcss-loader,从而看到编译时间的进一步下降。

对于我的 Web extension 项目,提升更为显著,在启用类型检查的情况下,我的台式机上编译时间从 11.65 秒减少到 3.6 秒(快 69%)

展望未来

随着 Rspack 临近 1.0 版本,我们又见证了两个基于 Rust 的构建工具 —— Farm 和 Mako 的发布(它们似乎也起源于中国)。显然,还有未来的 webpack 替代品 Turbopack,以及 Rollup 的 Rust 版本 Rolldown。

这些工具中哪一个会获得更多用户的青睐,这值得期待。就我个人而言,我认为 Rspack 有很大的成功机会,因为:

  • webpack 大概是当今 Web 应用中最广泛使用的构建工具,而 Rspack 提供了最简单和最低风险的从 webpack 迁移的路径。这一点很容易被忽视,但希望这篇文章能展示出,即使对于像我这样复杂度适中的应用,迁移到一个不同的构建工具,即使是高度兼容的工具,也可能是相当繁琐的过程。

  • webpack 拥有丰富的插件生态系统,大多数插件都可以在 Rspack 中使用。我使用的许多服务,如 Relative CI 和 Bugsnag,都提供 webpack 插件,但不支持其他任何构建工具。因此,除非其他构建工具能提供与 webpack 插件 API 的兼容性,否则它们将处于劣势。

  • Rspack 已经足够快了。其他工具可能最终会稍快一些(有趣的是,Mako 和 Farm 都声称比 Rsbuild 快一点,但并没有展示它们与 Rspack 的对比),但如果你的构建时间已经只有 2~3 秒,即使快一个数量级,也不足以成为一个令人信服的切换理由。

随着 Rspack 临近 1.0 版本,我很期待看到它将如何被市场接受,以及团队会把它带向何方。

译者注:Rspack 1.0 将于 2024 年 8 月发布,尽请期待~

转载自:https://juejin.cn/post/7402554147276980224
评论
请登录