KARTEの分析システムのレガシーな開発環境を高速にする。pnpm, Rspackの導入で改善できたこと。

こんにちは。KARTE Webの開発を担当するチームでエンジニアをしているnaoyashigaです。

2025年の年初に行なった改善について書きます。

(筆が遅くて公開が遅れました。。。)

今回は歴史の長いKARTEの分析システムにおいてpnpm(9.12.3)とRspack(1.2.7)の導入を行いました。長年運用しているシステムでの課題やpnpmとRspack導入によって得られたメリットを共有できればと思います。

課題

KARTEの分析システムではフロントエンドはTypeSscript・JavasScriptをメインに、バックエンドはNode.js(20.12.2)を用いて開発しています。package管理ツールはnpm(10.5.0)を利用していました。開発体験上、いくつかの課題がありました。

課題1: npmパッケージのinstallに時間がかかる

KARTEの分析システムはモノレポ構成になっており複数画面のフロントエンドとバックエンドを一つのレポジトリで管理しています。モノレポ上で管理しているpackageの数は18あり、いくつかは5年以上前から存在していました。そのためインストールするnpmパッケージの数が多くインストールに時間がかかっていました。特に開発を行うローカルPCでのインストールに時間がかかることが開発体験の質の低下を招いているという問題がありました。

課題2: モノレポ環境を活かすことができていない

モノレポ構成ですがnpm workspaceのようなモノレポ最適化構成を適用できていない状態でした。これは社内ライブラリの「link-local-dependencies」の使用が背景にあります。これはsymlink作成によって異なるpackage間のnpm packageの共有を実現するというライブラリです。弊社のシステム構成上では有用なライブラリでしたが近年のモノレポ系の開発支援ツールの進化に追従しておらずworkspaceの導入が難しいという事情がありました。

課題3: webpackのbuildに時間がかかる

歴史的な経緯からモノレポ構成上のあるパッケージの責務が肥大化しておりそのパッケージのビルドに時間がかかっていました。前述の課題1と同様に開発ローカルPCでの環境構築に時間がかかるという問題がありました。またビルドにはwebpackを使っていましたが近年webpackは積極的にアップデートされておらず長期的には他のbuildツールに移行しなければ運用が難しいという問題もありました。

解決策

前述の課題を解決するためにいくつかの改善を行いました。

解決策1: pnpmの導入によるインストール速度の改善

pnpmはnpmよりもパッケージのインストールが高速です。npmの場合、あるnpmパッケージを使うプロジェクトが100個ある場合、そのnpmパッケージを100個保存します。一方pnpmではContent-addressable storeにファイルを保存します。このstoreでは異なるファイルのみを保存するのでバージョンが違うnpmパッケージでも同じ内容のファイルなら1つだけ保存します。結果としてnpmと比較してインストール対象のファイル数が減るのでインストールが高速になります。さらにディスク上のスペースも節約できます。

解決策2: pnpm workspaceの活用と社内ライブラリからの脱却

pnpm workspaceを使用するために、これまで利用していた社内ライブラリの「link-local-dependencies」の使用をやめました。pnpm workspaceは、モノレポ環境におけるパッケージ間の依存関係管理をより効率的に行えるよう設計されており、これによりモノレポのメリットを最大限に活かすことが可能になりました。

解決策3: webpackからRspackへの移行

肥大化したパッケージにおいてwebpackからRspackへの移行を行いました。RspackはRustで書かれており並列処理を積極的に活用することで高速なビルドを実現します。また対象の肥大化したパッケージにおけるnpmパッケージの構成上、viteよりもwebpackからのマイグレーションが比較的容易であることも大きな選定理由でした。

やらないことを決めた

今回の改善のスコープを明確にする上で事前にやらないことを決めました。スコープが広すぎると動作検証の範囲が広くなり時間がかかるなど長期化し、いつまで経っても終わらない恐れがあるためです。またあまりにも長いと進捗が鈍く開発者のモチベーションも下がります!!(大事)

なので以下の2点はスコープから除外することにしました。

pnpm catalogsの導入見送り

pnpm catalogsはcatalogにnpmパッケージのバージョンを指定することでpnpm workspace環境においてnpmパッケージを再利用することができる機能です。例えばTypeScriptのバージョンを複数のパッケージ間で揃えるといったようなことができます。今回はpnpmに移行すること自体のコストが大きかったのでcatalogsは使いませんでした。またアップデートを行った2024年年末- 2025年1月ではpnpm catalogsの導入事例が少なかったため導入を見送ったという経緯もあります。現在2025年11月では社内の複数システムでpnpm catalogsが導入されていることもありKARTEの分析システムでも利用を計画しています。

パッケージ分割の最適化を除外

現在、コードベースは歴史的な経緯から複数のパッケージに分割されていますが全体を俯瞰した上での依存関係を考慮した最適な分割はまだ実現できていません。パッケージ分割の最適化はnpmパッケージのアップデートを容易にしたり並列ビルドによるCIの高速化といったメリットをもたらします。しかしこの最適化にはコードの広範囲な変更が伴いマージにかなりの時間を要する見込みです。そのため今回の改善フェーズからは除外することに決定しました。

pnpmへの移行

課題1と2のインパクトが大きいことから先にpnpm移行を行い、次にRspackへの移行を行いました。

まずは、早く失敗してみる

モノレポの規模が大きいため、事前に把握できない問題があるだろうと予想していました。以前のNode.jsのバージョンアップの際にも同様の経験があったため、まずは早く失敗し課題を特定していく方針で進めました。

一部のpackageのvupが難しい → npm管理とする

いくつかのパッケージでバージョンアップが難しいことが判明しました。これらは一旦、npm管理のままにすることにしました。

一つ目のパッケージ

rollupのバージョンが0.48.2と古く、関連するnpmパッケージの更新やrollup.config.jsの修正に時間がかかりそうだったため一旦npm管理としました。依存するnpmパッケージの量も少ないことからnpmのままでもインストール時間の最適化に大きな影響はないと判断しました。

二つ目のパッケージ

こちらのパッケージはコンテナ環境で起動するブラウザ内でスクリーンショット画像を撮影するというものです。pnpm管理にしてinstall、buildは問題なかったのですが検証環境でスクリーンショットが撮影できないことがあり原因の特定が難しい状況でした。pnpm移行のボトルネックになっていたのでnpm管理にすることにしました。このパッケージは歴史的な経緯によりKARTEの分析システムのパッケージの一つとして存在していましたがスクリーンショット撮影という責務だけを担っていることから近い将来にはKARTEの分析システムから独立させる計画があります。これによりKARTEの分析システムのpnpm環境最適化を進める予定です。

localDependenciesの利用をやめる

社内ライブラリ「link-local-dependencies」では、パッケージの特定のディレクトリにシンボリックリンクを貼っていました。

package.jsonにおいてpostinstall時に@plaidev/link-local-dependenciesを実行することでlocalDependenciesフィールドで設定したパッケージ名とディレクトリがリンクされるという仕組みです。

  "localDependencies": {
    "@plaidev/sample-package": "../../../../sample-package/build"
  },
  "scripts": {
    ...
    "postinstall": "npx -y --userconfig .npmrc @plaidev/link-local-dependencies",
    ...
  },

pnpm workspaceではパッケージの特定のディレクトリのみを参照する方法が提供されていません。このため、pnpm workspace移行の際にlocalDependencies経由でインストールしていたパッケージのimport pathを変更する必要がありました。

  "localDependencies": {
    "@plaidev/sample-package": "../../../../sample-package"
  }

この変更により大量の差分が発生するため、事前にパス変更のPRを作成し、複数PRに分けて修正を進めました。規模の大きいパッケージも含まれていたため、変更ファイル数は約1000に及びました。

pnpm workspace移行後は以下のようになります。

まずpnpm-workspace.yamlでworkspace対象packageを定義します。

packages:
  - packages/sample-package

次にpackages/sample-package/package.jsonにてパッケージの名前を定義します。

{
  "name": "@plaidev/sample-package",
  ...
}  

最後にpackage.jsonでworkspaceを指定します。

  "dependencies": {
	  "@plaidev/sample-package": "workspace:*",
	}

npmパッケージのバージョンアップ

pnpmに移行するとnpm時より依存解決が厳密になり、バージョンアップが必要なnpmパッケージがありました。

vue-template-compiler

vue-template-compilerでVueのバージョンをfileプロトコルで指定している部分に問題がありました。

    "devDependencies": {
        "vue": "file:../.."
    }

これはvue-template-compilerと同じnode_modulesフォルダ内にあるVue.jsを使用するということになります。pnpmではルートのnode_modulesにパッケージが配置されます。このときVue.jsのバージョン2系とVue.jsのバージョン3系の両方がルートのnode_modulesに存在する場合、vue-template-compilerが正しいバージョンのVue.jsを利用することができない場合があります。

この問題に関してpnpmのpackageExtensionで明示的にVue.js 2のバージョンを指定すると解決できました。

ルートのpackage.jsonに以下を設定しました。

{
...
  "pnpm": {
    "packageExtensions": {
      "vue-template-compiler@2.6.14": {
        "peerDependencies": {
          "vue": "2.6.14"
        }
      }
    }
  }
}

その他のnpmパッケージ

lesscsvなど、フロントエンドでの出力に大きな影響を与える可能性のあるパッケージについてはバージョンアップ用のPRを作成し入念な動作確認を行いました。

Webpack関連

vue-demiの設定

あるパッケージのbuild時に以下のエラーが出ていました。

Module not found: Error: Can't resolve 'vue-demi' 

webpack の設定ファイルを調査すると以下の alias が記述されていました。

resolve: {
    alias: {
     // ...
      'vue-demi': 'vue-demi/lib/index.esm.js'
    },
    ...
}

この設定は、webpack がバージョン 4.41.2pinia がバージョン 2.0.4 だった当時の環境で導入されたものです。vue-demi はVue 2 と Vue 3 の両方で動作するブリッジ機能を提供するライブラリでありpinia の内部で利用されています。

当時pinia はES Modules (ESM) を優先するライブラリでしたがimport 構文で vue-demi を参照している箇所がなぜか webpack によってCommonJS (CJS) として解決されバンドルされてしまう問題が発生していました。この問題を回避するため、alias を用いて vue-demi のESM版 (vue-demi/lib/index.esm.js) を明示的に指定することでモジュール依存関係を正しく解決していました。

しかし今回のpnpm移行前の段階でwebpack はバージョン 5.72.0pinia はバージョン 2.0.22 にそれぞれアップデートされていました。これらのバージョンでは、上記の alias 設定は不要となっていたため、削除しました。

意図しないnpmパッケージのインポートを発見

pnpmに移行することでpackage.jsonに明示的に書かれていないnpmパッケージを利用しているケースを発見することができました。(参考: pnpm作者による記事

date-fnsをtypoしていた

date-fnsdata-fnsとtypoしていたことが判明しました。偶然にもdata-fnsというライブラリが存在し、さらに他のnpmパッケージがdate-fnsを使用していたため、そちらが呼び出されていたために、これまでtypoに気づくことがありませんでした。

dnd-kit/utilities

npm管理ではdnd-kit/coreutilitiesにも依存してインストールされていたため、問題なく動作していました。しかし、pnpm移行後は明示的な依存関係の宣言が必要となり、dnd-kit/utiitiesのinstallが必要でした。

npm管理の時にcookieというパッケージを指定していましたが、npmのフラットなnode_modules構造によってたまたまcookieというライブラリが存在し、それを読み込んでいた可能性があります。本来はcomponent-cookieが正しく、package.jsonでも"component-cookie": "1.1.1"が指定されていたため、修正を行いました

Rspackへの移行

公式のmigration手順を参考にする

Rspack公式ドキュメントにwebpackからのマイグレーション手順に従ってrspackインストールやloaderの置き換えを進めます。

https://rspack.rs/guide/migration/webpack

プラグインについては「Plugin compatibility」を参考にします。

https://rspack.rs/guide/compatibility/plugin

Rspackでは多くのwebpackプラグインに対して互換性があることがわかります。

Karmaはwebpackで動かす

Rspack環境だとテストランナー「Karma」を使ったテストにおいてDOM APIを使うテストが実行できない不具合がありました。調査の結果、DOM APIに依存しないNode.jsのAPIのみで実行できるテストはRspackでも問題ないことがわかりました。そこでDOM API依存のテストは別でwebpack実行環境を作成することにしました。理由としてはKarmaは新規開発が停止していること、公式のkarma-rspackは存在せず作成予定もないことからRspack環境でKarmaを利用することが難しいと判断したからです。テストのためにwebpack環境を別で作成したことで認知負荷が高まりメンテナンスがしづらいという問題はあります。これについては将来的にKarmaを使わないテストコードを作成することで解決する予定です。

改善の結果

ローカルPCでのインストール・ビルド時間の削減

開発ローカルPC(Apple M1 Max, メモリ64GB)において改善前後の計測も行いました。

インストールの改善

KARTEの分析システム全体のインストール時間を計測しました。

  • 改善前 (npm): 13分6秒 (786.47秒)
  • 改善後 (pnpm): 6分18秒 (378.09秒)
  • 改善率: 51.93%の時間短縮、約1.99倍の高速化

ビルドの改善

webpackからRspack移行における前後比較も行いました。

  • 改善前 (webpack): 1分48秒 (108.07秒)
  • 改善後 (rspack): 26.837秒
  • 改善率: 75.17%の時間短縮、約4.03倍の高速化

CIの高速化

CIでの改善は前述のローカル開発環境の改善と比較すると改善量はやや少なくなります。これはCIで複数のジョブが稼働しており今回の改善の影響をあまり受けないジョブもあるからです。(後述の図参照)

改善前後の三ヶ月において各項目における平均時間を比較しました。
successci.png

上図よりCIのジョブの構成は

  • e2eテスト環境構築に必要なジョブ群
  • e2eテストジョブ
  • その他のジョブ

に分けられます。

①CI全体の時間

  • 改善前: 14.70 min
  • 改善後: 13.97 min
  • 改善率: 4.97%の時間短縮、約1.05倍の高速化

②e2eテスト環境構築

  • 改善前: 5.66 min
  • 改善後: 4.61 min
  • 改善率: 18.55%の時間短縮、約1.23倍の高速化

③肥大化パッケージのビルド

  • 改善前: 5.15 min
  • 改善後: 2.74 min
  • 改善率: 46.80%の時間短縮、約1.88倍の高速化

Rspackへの移行により「肥大化パッケージのビルド」が特に高速になりました。また移行前は「e2eテスト環境構築」のボトルネックが「肥大化パッケージのビルド」でしたが、高速化したことにより別のジョブがボトルネックになりました。この新しいボトルネックは今回の改善の範囲外の部分で時間がかかっているためCI全体としてはあまり高速化できていません。近い将来、新しいボトルネックも早くすることでCI全体の短縮も行いたいと思います。

CI失敗時の比較

errorstatus.png

CI失敗時の実行時間を改善前後で比較します。インストールとビルドが速くなることでCI失敗検知の時間も短くなったことがわかります。

①CI全体の時間

  • 改善前: 12.10 min
  • 改善後: 12.04 min
  • 改善率: ほぼ変わらず

②肥大化パッケージのビルド

  • 改善前: 3.01 min
  • 改善後: 0.80 min
  • 改善率: 73.42%の時間短縮、約3.76倍の高速化

③e2eテスト環境構築

  • 改善前: 3.57 min
  • 改善後: 2.74 min
  • 改善率: 23.25%の時間短縮、約1.30倍の高速化。

CIのキャッシュ

pnpmについては.pnpm-storenode_modulesをキャッシュすることも試しました。インストールは確かに高速ですがキャッシュのsaveとrestoreに時間がかかることがわかりました。トータルの所要時間を計算するとキャッシュあり・なしで時間はほぼ変わりませんでした。実装を簡易的にするためキャッシュは用いていません。

Rspackについてもキャッシュを使っていません。キャッシュを使わなくてもwebpackと比較して十分高速だったからです。まずRspackではwebpackにあるようなfilesytem cacheをサポートしていません。メモリキャッシュはありますがproductionではデフォルトでfalseです。またPersistent cacheという永続的なキャッシュ機能がありますがexpreimentな機能であり不具合が起こったときの調査が難しいので採用していません。webpackではfileキャッシュのsave, restoreのために数分かかることもありましたがそれがゼロになったことが速度改善に大きく寄与しました。

これからの展望

今回はpnpm、Rspackへの移行を行いました。これからやれることは無数にあります。パッケージの依存関係の切り分けを最適にしてもっと並列でCIを回せるようにしたり、責務を小さくすることでnpmパッケージをアップデートしやすくもしたいです。気づけばpnpmのバージョンは10になったしcatelog機能も導入できるでしょう。Rspackもどんどん進化しており、Bytedance社のweb-infra-devチームの開発スピードは凄まじいです。Build analyzerのRsdoctorMCP Serverも登場しました。ビルド情報をAIに渡して改善を行ったり不具合調査にも役立てたいですね。