🚀

pnpmからBunへの移行でCI実行時間を64%削減した話

に公開

はじめに

私たちのプロジェクトでは、パッケージマネージャーとしてpnpmを使用していましたが、CI/CDパイプラインの実行時間が課題となっていました。特に、依存パッケージのインストールやテストの実行に多くの時間がかかり、開発効率に影響を及ぼしていました。

そこで、高速なパフォーマンスで注目を集めているBunへの移行を決断しました。本記事では、実際の移行プロセスと、その結果得られた大幅なパフォーマンス改善について詳しく解説します。

プロジェクト構成

本プロジェクトは、以下のような構成のモノレポです:

  • Backend: Hono.js + Prisma + TypeScript
  • Frontend: React + Vite + TypeScript
  • CI/CD: GitHub Actions
  • テスト: Bun Test(バックエンド)

移行前の課題

CI実行時間の増加

プロジェクトの規模拡大に伴い、以下のような課題が顕在化していました:

  1. テスト実行時間: 特にリポジトリ層のテストが1分以上かかる
  2. 依存パッケージのインストール時間: pnpm installに時間がかかる

これらの課題により、PR作成からマージまでのリードタイムが長くなり、開発速度に影響を与えていました。

Bunとは

Bunは、JavaScriptおよびTypeScriptのための高速なオールインワンツールキットです。以下の特徴を持ちます:

  • 高速なパッケージマネージャー: npmやpnpmと比較して圧倒的に高速
  • ビルトインテストランナー: Jest互換のAPIを持つ高速なテストランナー
  • バンドラー: 高速なJavaScript/TypeScriptバンドラー
  • Node.js互換: 既存のNode.jsエコシステムとの高い互換性

特に、Zigで実装された高速なランタイムと、効率的なパッケージ管理により、従来のツールと比較して大幅なパフォーマンス向上が期待できます。

移行手順

1. Bunのインストールとバージョン管理

まず、プロジェクトで使用するBunのバージョンを.tool-versionsファイルで管理することにしました:

.tool-versions
bun 1.2.14

また、各パッケージのpackage.jsonpackageManagerフィールドを追加し、使用するBunのバージョンを明示しました:

backend/package.json
{
  "name": "backend",
  "type": "module",
  "packageManager": "bun@1.2.14",
  // ...
}

2. パッケージのインストール

pnpmからBunへの移行は非常にシンプルです:

# pnpmのロックファイルと依存関係を削除
rm -rf node_modules pnpm-lock.yaml

# Bunで依存関係をインストール
bun install

Bunはpackage.jsonを読み取り、互換性のある形で依存関係をインストールします。bun.lockファイルが生成され、これが新しいロックファイルとなります。

3. スクリプトの更新

package.json内のスクリプトを、Bunコマンドに対応するよう更新しました:

package.json
{
  "scripts": {
    "dev": "bun --watch src/index.ts",
    "build": "bun build src/index.ts --outdir dist --target bun",
    "test": "bun test",
    "fmt": "bunx @biomejs/biome format --write .",
    "lint": "bunx @biomejs/biome lint ."
  }
}

bunxコマンドは、npxpnpm dlxに相当するコマンドで、パッケージを一時的にインストールして実行します。

4. GitHub Actionsの更新

GitHub ActionsのワークフローをBunに対応させました。主な変更点は以下の通りです:

Bunのセットアップ

.github/workflows/ci-repositories.yml
- name: Setup Bun
  uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
  with:
    bun-version-file: ".tool-versions"

キャッシュキーの更新

pnpmからBunへの移行に伴い、キャッシュキーも更新しました:

.github/workflows/ci-repositories.yml
- name: Get node_modules cache
  uses: actions/cache@v4
  with:
    path: |
      **/node_modules
    key: ${{ runner.os }}-bun-${{ hashFiles('backend/bun.lock', 'backend/package.json') }}
    restore-keys: |
      ${{ runner.os }}-bun-${{ hashFiles('backend/bun.lock') }}

依存関係のインストール

.github/workflows/ci-repositories.yml
- name: Install dependencies
  run: bun install --frozen-lockfile

--frozen-lockfileオプションは、pnpmの--frozen-lockfileやnpmのciコマンドに相当し、ロックファイルを更新せずに依存関係をインストールします。

移行後の効果

テスト実行時間の劇的な改善

最も顕著な改善が見られたのは、リポジトリ層のテスト実行時間です:

項目 移行前(pnpm) 移行後(Bun) 削減率
Repository Layer Tests 1m 5s 23s 約64%削減

実行時間の比較

移行前(pnpm) 移行後(Bun)
移行前: 1分5秒 移行後: 23秒
1分5秒 23秒

この改善により、PRのフィードバックループが大幅に短縮され、開発速度が向上しました。

なお、今回は具体的な数値データがあるリポジトリ層のテストのみを紹介していますが、実際には他のCIワークフロー(ビルド、デプロイなど)でも同様に実行時間の短縮が確認されています。Bunへの移行は、プロジェクト全体のCI/CD効率化に大きく貢献しました。

移行時の注意点と課題

1. ロックファイルの管理

pnpmからBunへの移行に伴い、ロックファイルがpnpm-lock.yamlからbun.lockに変更されます。移行時には、以下の点に注意が必要です:

  • 既存のロックファイルの削除: pnpm-lock.yamlを削除してからbun installを実行
  • バージョンの固定: 依存パッケージのバージョンが変わらないことを確認
  • Git管理: bun.lockをGitにコミット

2. CI/CDの段階的な移行

すべてのワークフローを一度に移行するのではなく、段階的に移行することをお勧めします:

  1. ローカル開発環境での検証: まずローカルでBunを試す
  2. 非クリティカルなワークフローから移行: lintやformatチェックなど
  3. テストワークフローの移行: テストが正常に動作することを確認
  4. デプロイワークフローの移行: 最後にデプロイ関連のワークフローを移行

3. 互換性の確認

Bunは高いNode.js互換性を持ちますが、一部のパッケージでは動作しない可能性があります:

  • ネイティブモジュール: 一部のネイティブモジュールは動作しない可能性がある
  • 特定のNode.js API: 一部のNode.js APIはまだ実装されていない可能性がある

私たちのプロジェクトでは、以下のパッケージで問題なく動作しました:

  • Prisma(ORM)
  • Hono(Webフレームワーク)
  • Vite(ビルドツール)
  • Biome(Linter/Formatter)

4. テストランナーの移行

Bunは独自のテストランナーを持っており、Jest互換のAPIを提供しています。私たちのプロジェクトでは、以下のように移行しました:

// 移行前(Jest)
import { describe, it, expect } from '@jest/globals';

// 移行後(Bun)
import { describe, it, expect } from 'bun:test';

ただし、多くの場合、インポート文を変更する必要はなく、Bunが自動的に互換性を提供します。

5. キャッシュ戦略の見直し

Bunは独自のキャッシュメカニズムを持っているため、GitHub Actionsのキャッシュ戦略を見直す必要がありました:

# 最適化されたキャッシュ設定
- name: Get node_modules cache
  uses: actions/cache@v4
  with:
    path: |
      **/node_modules
    key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
    restore-keys: |
      ${{ runner.os }}-bun-

移行のベストプラクティス

1. バージョン管理の徹底

.tool-versionsファイルとpackage.jsonpackageManagerフィールドを使用して、Bunのバージョンを明示的に管理しましょう:

package.json
{
  "packageManager": "bun@1.2.14"
}

2. ロックファイルの固定

CI/CDでは必ず--frozen-lockfileオプションを使用して、ロックファイルを固定しましょう:

bun install --frozen-lockfile

3. 段階的な移行

すべてを一度に移行するのではなく、段階的に移行することで、問題が発生した際の切り戻しが容易になります。

4. パフォーマンスの測定

移行前後でパフォーマンスを測定し、改善効果を定量的に評価しましょう。GitHub Actionsの実行時間を比較することで、効果を可視化できます。

まとめ

pnpmからBunへの移行により、**リポジトリ層のテスト実行時間が約64%削減(1m 5s → 23s)**という大幅な改善を実現できました。

この改善により、開発者のフィードバックループが短縮され、開発効率が大幅に向上しました。

Bunは、高速なパフォーマンスとNode.jsとの高い互換性を持ち、既存のプロジェクトへの導入も比較的容易です。特に、CI/CDの実行時間が課題となっているプロジェクトでは、Bunへの移行を検討する価値があるでしょう。

ただし、移行にあたっては、段階的なアプローチを取り、互換性の確認やテストの実施を十分に行うことが重要です。

参考資料

Discussion