TypeScriptモノレポを加速させる:ビルドパイプライン高速化の実践ガイド

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

スケーリングの代償:なぜモノレポは停滞するのか

モノレポはシームレスなコード共有を約束しますが、CI/CDパイプラインがストレスの溜まるボトルネックになることがよくあります。以前私が参加したチームでは、共有ロギングユーティリティのたった1行の変更で、リポジトリ内の全アプリケーションの15分間に及ぶ再ビルドがトリガーされていました。生産性が低下するだけでなく、実際には変更されていないコードをコンパイラが再処理するのを待つために、CIの予算を浪費していたのです。

Lernaや標準的なnpmワークスペースのようなツールは依存関係の管理には優れていますが、実行を最適化するためのインテリジェンスが不足しています。これらは、App Bを変更したとしてもApp Aが有効なままであることを認識しません。Turborepoはこのギャップを埋めます。これは、依存関係グラフをマッピングし、可能な限りのタスクをキャッシュするオーケストレーション層として機能します。

これらのオーケストレーション層をマスターすることは、個人アプリからエンタープライズインフラへと進むエンジニアにとっての通過儀礼です。ビルドに3分以上かかるなら、集中力と資金を失っています。スピード感のあるチームでは、20分のビルドを3分に短縮することで、毎週数十時間のエンジニアリング時間を節約できます。

はじめに:ワークスペースの初期化

Turborepoを理解する最も簡単な方法は、新規プロジェクトでの動作を見ることです。既存のリポジトリに手動で統合することもできますが、公式のスターターを使用するのが、ディレクトリ構造のメンタルモデルを構築する上で最も信頼できます。

# 新しいTurborepoワークスペースをスキャフォールディングする
npx create-turbo@latest my-monorepo

インストールが完了したら、主要な構造を確認しましょう:

  • apps/: Next.jsサイトやViteダッシュボードなど、デプロイ可能なプロジェクト。
  • packages/: UIコンポーネント、TypeScript設定、ユーティリティ関数などの共有ライブラリ。
  • turbo.json: ビルドシステム全体を制御するコマンドセンター。

共有設定は、ここでの最大のメリットです。個別のtsconfig.jsonファイルを維持する代わりに、packages/tsconfigからベース設定をエクスポートして拡張できます。これにより、手動の監視なしに、チームの全開発者が同じ厳格なルールに従うことが保証されます。

turbo.jsonでのビルドパイプラインの設定

すべてはturbo.jsonpipelineオブジェクトを中心に展開されます。ここでタスクの依存関係をマッピングします。TypeScriptプロジェクトでは、ビルド順序は譲れません。webアプリがuiパッケージをインポートしている場合、そのパッケージが最初に準備されている必要があります。

プロダクション対応のモノレポ向けに最適化された設定例を見てみましょう:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**", "build/**"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    }
  }
}

主要なロジックの解説:

  • カレット記号 (^build): パッケージのビルドが、その依存関係のビルドが先に完了していることに依存していることをTurboに伝えます。これが安定したグラフベースの実行の秘訣です。
  • outputsの定義: 保存するフォルダをTurboに正確に伝えます。前回の実行から入力が変更されていない場合、Turboは数ミリ秒でキャッシュからこれらのフォルダを復元します。
  • 選択的キャッシュ: devタスクではcache: falseを設定します。開発サーバーは継続的なプロセスであるため、保存すべき静的な出力が存在しないからです。

TypeScriptのパフォーマンス戦略

多くの開発者が、ローカル開発中にすべてのパッケージで個別のtscを実行させてしまうという間違いを犯します。これは不要なオーバーヘッドを生みます。代わりに、次の2つの調整を試してみてください:

  1. Just-in-Timeトランスパイルの使用: 内部の共有パッケージについては、事前コンパイルを行わないでください。Next.js(transpilePackages経由)やViteなどのツールは、生の.tsファイルを直接読み込むことができます。これにより、ホットリロードのループが大幅に高速化されます。
  2. 型チェックの切り離し: tsc --noEmitを独自のパイプラインタスクとして扱います。リンティングと並行して実行されるtype-checkタスクを追加することで、型検証が実際のビルド成果物の生成をブロックしないようにします。
"type-check": {
  "dependsOn": ["^build"],
  "outputs": []
}

モニタリングとリモートキャッシュ

セットアップを確認するには、ビルドコマンドを2回実行します。1回目は通常通りすべて実行されます。2回目はほぼ瞬時に完了し、ターミナルに>>> FULL TURBOステータスが表示されるはずです。

# 初回実行(キャッシュミス)
npx turbo build

# 即時の再実行(キャッシュヒット)
npx turbo build

標準的なキャッシュはローカルマシン上でのみ動作します。GitHub ActionsなどのCI環境では、リモートキャッシュを使用しない限り、実行間でキャッシュが失われます。VercelやセルフホストのS3バケットなどのプロバイダーに接続することで、CIサーバーは開発者がローカルで作成した成果物を取得できます。この機能を有効にするだけで、月間のコンピューティング費用を400ドル削減したチームを見たことがあります。

タスクが遅いと感じる場合は、--summarizeフラグを使用してください。.turboフォルダに詳細なJSONレポートが生成され、なぜキャッシュミスが発生したのかを正確に説明してくれます。ビルドが遅いと感じたときに私が最初に使うツールです。

npx turbo build --summarize

リポジトリが大きくなるにつれて、プロジェクトのgraph(グラフ)を可視化することも同様に有用です。npx turbo build --graphを実行してDOTファイルを生成します。これを可視化ツールに貼り付けることで、パイプラインを遅らせている循環依存や不要な結合を特定するのに役立ちます。

長期的なメンテナンス

Turborepoは「一度設定すれば終わり」のソリューションではありません。規模が拡大するにつれて、共有パッケージは軽量に保ちましょう。パッケージが小さいほど、システムの他の部分を変更したときにキャッシュヒットする可能性が高くなります。Turboが変更を正確に追跡できるように、常にpackage.jsonのエクスポートを明示的に定義してください。

グラフベースのシステムを採用することは、ツールとの戦いをやめることを意味します。最終的には開発サイクルが短縮され、CIのコストも大幅に削減されるでしょう。コードに集中し、オーケストレーション層に重労働を任せましょう。

Share: