🦀

ニコニコ生放送のアプリケーションにRspackを導入している話

2024/12/05に公開

この記事はニコニコ生放送のフロントエンドのアプリケーションにRspackを導入にあたって直面した問題やその解決策について時系列でまとめたものです。
以前書いた既存のアプリケーションに Rspack を導入しようとして失敗した話の記事と重複するところもありますが、記事を書いた時点では達成できなかった本番環境での実践投入を果たすにあたって新たな試行錯誤もあったのでリライトという形で本記事を書いています。

why Rspack?

ニコニコ生放送のフロントエンドではWebpackを利用して成果物を生成しているのですが、ビルドの遅さによって開発体験が悪化しており、その改善策を模索していました。
問題の改善策として、以前にViteを導入することで開発時のビルド速度の改善を試みたことがあったのですが、css modulesのcomposesという機能で読み込んだscssがcssとして取り扱われてしまう問題に直面してしまい、導入を断念していました。
https://github.com/vitejs/vite/issues/10340
一応修正PRを出してみたりはしたのですが、エッジケースの考慮など不足している点があってマージには至りませんでした。
こうして問題意識はあるものの根本的な解決には至らず、小手先のパフォーマンスチューニングなどでお茶を濁していたところ、Rspackの発表がされたため導入を検討してみることにしました。
この時点ではRolldownによる高速化されたViteの開発は発表されておらず、高速なフロントエンドビルドを実現をうたう唯一の存在でした。

Rspack導入への道

現在では

- const webpack = require('webpack');
+ const webpack = require('@rspack/core');

のようにWebpackの代わりにRspackを読み込めばほぼ問題なく動いてくれるRspackですが、検証を始めたころのRspackのバージョンは0.1.xで、Webpackのコンフィグとは互換性がない部分や動かないloaderやpluginも多々ありました。
そういった動かないもののうち必須でないものはコンフィグから排除していくことで検証を進めました。

import { defineConfig } from "@rspack/cli";
import webpackConfig from "./webpack.config";
export default defineConfig({
  ...webpackConfig,
  // 動かないものは雑に消してしまう
  plugins: webpackConfig.plugins?.filter(plugin => {
  if (plugin instanceof MiniCssExtractPlugin) {
    return false;
  }
  if (plugin instanceof webpack.ProvidePlugin) {
    return false;
  }
  if (plugin instanceof BundleAnalyzerPlugin) {
    return false;
  }
  return true;
  })
});

検証を進めていく中で発覚した問題は最小の再現コードを作った上でどんどんissueにしていきました。
https://github.com/web-infra-dev/rspack/issues/3611
https://github.com/web-infra-dev/rspack/issues/3777
https://github.com/web-infra-dev/rspack/issues/3875
https://github.com/web-infra-dev/rspack/issues/3724
作成したissueを見ていただけるとわかるように、ほとんどはcssのビルドに関する問題でした。
当時のRspackはcssのビルドにswc_cssを利用していましたが[1]、lightningcssに比べてユーザーが少ないためか未発見のバグが多々あるようでした。swc_cssのownerであるkdy1氏もswc本体に注力しているためswc_cssに関するissueを作っても優先度低めの対応となるようでした。
ということで自分で直すことにしました。
https://github.com/swc-project/swc/pull/7670
https://github.com/swc-project/swc/pull/7917
コードベースの規模が大きいこととRustを触るのが初めてだったのでなかなか苦労はしましたが、無事修正をマージしてもらうことができました。
これらの修正により、無事アプリケーションを問題なく動作させることができるようになりました。
この時点でのビルド速度はWebpackが1分30秒程度かかるのに対してRspackでは40秒程度と約3倍の高速化に成功しました。しかしながら、Rspackはpersistent cachingの仕組みを持っていないため、開発時に複数回ビルドを行うような場合においてはWebpackのほうが高速であるという問題も判明していました。この問題についてはRspackの開発プランの策定のissueでpersistent cachingがあると助かるという話をして優先度を上げてもらう交渉をしたりしました。[2]
https://github.com/web-infra-dev/rspack/issues/4343#issuecomment-1766396092

Rspack v1.0のリリース

persistent cachingがないことで一旦導入を足踏みしていたことと、某サイバー攻撃によってそれどころではない状況となってしまいしばらく進捗はありませんでしたが、Rspack v1.0がリリースされたタイミングで再度導入に向けて動き出しました。
v1.0ともなると利用しているloaderとpluginで動かないものは無くなっており、webpack.config.jsをほぼそのまま使える状態になっていました。
webpackのAsset Modulesに相当する部分のコンフィグなど一部非互換なところがあるのでdrop-in replacementというわけにはいきませんでしたが、ドキュメントも充実しているのでそこまで苦労することはありませんでした。ビルド速度もさらに高速化しており、Webpackと比べて4倍以上高速になりました。ただ、検証をしていた0.xの時にはなかった新たなバグにも直面することにもなりました。いずれもcloseされていないissueですがworkaroundはわかっているので解説します。
https://github.com/web-infra-dev/rspack/issues/8057
この問題はcssとjsの両方を提供しているパッケージをcssからimportする際にcssではなくjsをimportしてしまいエラーになる問題です。
調査をしたところ1.0.0-alpha.3からmoduleの解決にenhanced-resolveを使わないようになったことが原因のようで、workaroundとしてcssとjsの両方を提供しているパッケージのpackage.jsonに

{
  "style": "./index.scss"
}

を追加することで回避できることがわかりました。

続いてswc-loaderのオプションのenv.mode: "usage"とrspack.CssExtractRspackPlugin.loaderを併用するとビルドが終わらない問題に遭遇しました。
https://github.com/web-infra-dev/rspack/issues/8291
この問題はcore-jsに対してcore-jsを挿入しようとするループになっていることが原因とメンテナーの方に教えていただいたので

{
  test: /\.js$/,
  exclude: [/core-js/],
  use: {
    loader: "builtin:swc-loader",
    env: {
      mode: "usage",
    }
  }
}

のようにexcludeでcore-jsに対してcore-jsが挿入されないようにすることで回避できました。

以上を持って問題を回避できたので順次導入を進めており、現在本番環境の一部ページですでにRspackでビルドされたコードが動いています。本年度中にはすべてのアプリケーションをRspackでビルドすることを目標に作業を続けています。

next step

Rspackにはcss-loader/postcss-loaderを使わずにcssをビルドできる仕組み(experiments.css)がありますが、今回のRspack導入においてはこの機能は利用しませんでした。というのも、cssをビルドする際に内部的にswc_cssではなくlightningcssをつかうのですが、lightningcssでは以下の問題が再発してしまっているからです。
https://github.com/web-infra-dev/rspack/issues/3875
swc_cssに出したPRと同じ対応を入れれば直ることはわかっているので、折りを見て対応しようと考えています。
Rspackのドキュメントによると、
https://rspack.dev/guide/optimization/profile#postcss-loader

postcss-loader compiles CSS code based on PostCSS, which is often used with PostCSS plugins to downgrade CSS syntax, add vendor prefixes, etc. You can replace PostCSS with the faster Lightning CSS by using Rspack's built-in builtin:lightningcss-loader.

とのことで、置き換えることで更なる高速化が期待できそうですが、ビルド時間の3分の1を占めるsass-loaderを消すことはできないので高速化できても3秒程度になりそうだと見込んでいます。

まとめ

Rspackはcss modules周りに少々課題を抱えていることは事実ですが、多くのプロダクトにおいては比較的簡単にWebpackからの移行が行えるはずです。非常にコスパの良い改善ですのでチャレンジしてみることをお勧めします。

脚注
  1. 現在ではlightningcssが使われています ↩︎

  2. この記事の執筆時点でもまだpersistent cachingのリリースはされていませんが、v1.2でついにリリースされるようでとても楽しみにしています。
    https://x.com/rspack_dev/status/1855840964193169689 ↩︎

Discussion