kubell Creator's Note

ビジネスチャット「Chatwork」のエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

フロントエンドのビルドをRspackで気軽に高速化した話

フロントエンド開発部の須田です。

この記事は kubell Advent Calendar 2024 (シリーズ1) 12月4日の記事です。 前日はこちらの2本でした。

creators-note.chatwork.com creators-note.chatwork.com

AWS Amplify を使った爆速開発も楽しそうですし、ゴリゴリとパーサーコンビネーターを書くのも楽しそうですね!年末年始は寝てられません。

さて、僕が所属する kubell のフロントエンド開発部では、他のチームが Chatwork のフロントエンドを開発する際に予期される障害や無駄を取り除く、いわばフロントエンド限定のプラットフォームチームのような仕事もメイン業務として取り扱っています。

今回、開発者が開発時にローカルでフロントエンドをビルドするのが遅く開発体験が悪いという問題を軽減するために、バンドラを webpack から Rspack に乗り換えてみたので、報告します。

Rspack とは?

Rspack(/'ɑrespæk/ と発音)は、webpack のエコシステムとの強力な相互運用性を提供する高性能なRustベースのJavaScriptバンドラーであり、より迅速な開発サイクルと2つのツール間の効率的なコラボレーションを実現します。

https://www.rspack.dev/guide/introduction.html#introduction

github.com

www.rspack.dev

公式サイトの説明どおり、Rspack は webpack 互換を目指す、Rust 製の JavaScript バンドラーです。webpack 互換に関して言えば、次のような特徴を持っています。

  • webpack とほぼ同じ構造で設定を記述できる
  • webpack の主要なプラグインやローダーをそのまま動作させることができる

Rspack は組み込みで SWC のローダーを持っており、webpack の babel-loader の代わりにこれを使うことで高速に JavaScript / TypeScript / JSX をトランスパイルすることができます。

Chatwork のフロントエンドの現状

Chatwork のフロントエンドのソースコードは歴史的経緯から、次のように様々な種類のファイルで構成されています。

  • .js ファイル
    • 開発の超初期段階から使われている、 jQuery に依存した処理
    • require で即時関数を発火して展開している
  • .scss ファイル
    • 古いUIのスタイリング
  • .ts ファイル
    • 通常のロジック
  • .tsx ファイル
    • Reactコンポーネント
    • styled-component でスタイリング
  • .graphql ファイル
    • GraphQL / Relay のデータ定義

これらの多種多様なファイル群を webpack で “よしなに” 処理しているのが現状です。

webpack の構成

Chatwork の webpack の設定は、簡略化すれば次のようになっています。

TypeScript や JavaScript は Babel で処理し、スタイルは sass-loader から style-loader までの一連の流れで処理しています。また、いくつかプラグインを入れており、トランスパイルに介入してビルド時の情報をプロダクト内で使えるようにしたり、TypeScript の型チェックをしたりしています。

module.exports = {
  devtool: 'inline-source-map',
  plugins: [
    new webpack.ProvidePlugin({
      // some entries...
    }),
    new webpack.EnvironmentPlugin({
      // some entries...
    }),
    new webpack.DefinePlugin({
      // some entries...
    }),
    new ForkTsCheckerWebpackPlugin(),
  ],
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              plugins: [
                'babel-plugin-styled-components',
              ],
            },
          },
        ],
      },
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
          },
          {
            loader: 'source-map-loader',
          },
        ],
      },
      {
        test: /\.scss$/,
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              sourceMap: false,
            },
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
            },
          },
          {
            loader: 'import-glob-loader',
          },
        ],
      },
    ],
  },
};

Rspack への移植

なぜやるか

現状でも問題無く動作はしているのですが、次のような理由で Rspack を入れてみることにしました。

  • 開発用のビルドが生成されるまでに20秒程度かかっており、微妙に遅い
    • webpack とは無関係だが、事前準備のスクリプトも含めると40秒程度
  • Rspack 使ってみたい (重要)
    • webpack の設定は比較的シンプルなので、Rspack への移植が容易そう

Vite や Turbopack を使うことも考えたのですが、これらのツールは (当然ながら) webpack と互換性がなく、移植コストが高いと判断しました。特に、Chatwork のコードベースは require() によるモジュールの読み込みを含んでいるため、require() を使えないツールは今回は見送り1としました。

また、今回 Rspack に移植するのは開発者がローカルマシンでビルドする際に用いる、開発用ビルドの設定のみです。本番用ビルドは CI で動作しているので多少時間がかかっても問題が大きくないことと、毎日116.3万ユーザーがアクセスする プロダクトのビルドシステムを壊すと損害が甚大であることから、リスク-リターンのバランスを考慮して、本番用ビルドでの採用は見送りました。

移植作業

Rspack が webpack からのマイグレーションガイドを用意してくれているので、基本的にはそれに従えば問題ありません。

Migrate from webpack - Rspack

ただし、Chatwork では次のとおりの変更を加えました。

style-loader の使用

Rspack は CSS をネイティブでサポート しており、webpack の css-loader や style-loader は必要ありません。例えば次のように書けば、自動で SASS から CSS を生成してくれます。

// rspack.config.js module.rules
{
  test: /\.scss$/,
  use: [
    // css-loader や style-loader は不要
    { loader: 'sass-loader' },
  ],
  type: 'css',
}

しかし Chatwork では (これまた歴史的経緯から) スタイルを CSS として個別に生成せず、 style-loader で CSS-in-JS として JavaScript に組み込む構成になっています。

現時点では Rspack にはスタイルを CSS-in-JS として扱うようにする設定項目は見当たらないので、従来どおり css-loader や style-loader で処理するようにしました。

プラグインの対応

Chatwork で使用している webpack プラグインのうち、webpack が組み込みで用意しているプラグインはそのまま利用できました。

具体的には、次のような設定だったものを 、

// webpack.config.js
plugins: [
  new webpack.ProvidePlugin({
    // some entries...
  }),
  new webpack.EnvironmentPlugin({
    // some entries...
  }),
  new webpack.DefinePlugin({
    // some entries...
  }),
],

次のように書き換えます。

// rspack.config.js
plugins: [
  new rspack.ProvidePlugin({
    // some entries...
  }),
  new rspack.EnvironmentPlugin({
    // some entries...
  }),
  new rspack.DefinePlugin({
    // some entries...
  }),
],

また、TypeScriptの型チェックのために用いている fork-ts-checker-webpack-plugin は記述の変更無しに、そのまま利用することができました。

GraphQL / Relay への対応

Chatwork では、通信の一部に GraphQL を導入するべく 準備を進めています

Chatwork で採用を検討している GraphQL のクライアントライブラリの Relay は Babel や SWC などトランスパイラに介入して、物ごとを整えてやる必要があります。

Rspack の Relay への対応方法はバージョンによって大きく揺れているのですが、現在の 1.x 系では @swc/plugin-relay使えばOK です。

// rspack.config.js module.rules
const rules = {
  test: /\.tsx?$/,
  use: [
    {
      loader: 'builtin:swc-loader',
      options: {
        ...swcTypeScriptConfig,
        jsc: {
          experimental: {
            plugins: [['@swc/plugin-relay', {}]],
          },
        },
      },
    },
  ],
}

結果

Rspack のおかげで、これまで webpack では約20秒かかっていた JavaScript のバンドルが約3秒で終わるようになりました。ついでに (webpack / Rspack とは関係ありませんが) 約40秒かかっていた前処理にも手を加え、こちらは約12秒に圧縮できました2

結果として、これまで1分近くかかっていた開発用ビルドが15秒程度になりました。開発用ビルドは開発者が日常的に、それこそ息をするように行う行為なので、1日あたりの短縮時間を考えると大きな効果があったと思います。

以前 改良後
前処理 約40秒 約12秒
JavaScriptのバンドル 約20秒 約3秒
合計 約60秒 約15秒

また、実は Rspack を採用したのは2024年2月のことで、これまで10ヶ月弱運用していますが、社内の開発者から不具合の報告は上がっていません。開発用ビルドと本番用ビルドで違うビルドシステムを採用したことに関してもフロントエンド開発部としては少々神経を尖らせていた部分ではあるのですが、この点に関しても問題はなさそうでした。

感想

かなり昔ながらの構成のフロントエンドアプリケーションにも関わらず、プロダクションコードに一切手を入れず設定ファイルを書くだけで Rspack に対応できたのは率直に言って驚きました。「でも設定ファイルは書いてるじゃん」とツッコミをもらいそうですが、そうは言っても Rspack の webpack 互換は伊達ではなく、ほとんど機械的に設定ファイルを移植するだけでサッとビルドできてしまいます。

これから開発を始めるプロダクトは Vite や Turbopack を使ってスマートにビルドをすれば良いと思いますが、webpack と長年付き合ってきた既存プロダクトに簡単にテコ入れをしたい場合、Rspack は強力な味方になってくれるのではないかと思います。


  1. まぁ、おそらく dynamic import に変えれば動くのですが。個人的には現在 Jest で動いているテストランナーを vitest に変えたいと思っているので、それと併せて Rspack → Vite の移行もできたら良いな〜と妄想しています。
  2. 「"ついで" のほうが効果が大きいじゃないか!」とおっしゃるそこのあなた、その通りです。ただ、直列だった処理を並行処理にしただけなので特に記事にできるようなこともなく…。