午前2時15分のホットフィックスの悪夢
それは火曜日の午前2時15分のことでした。本番環境で致命的なバグが発生し、ユーザーの約15%でチェックアウトページがクラッシュしていました。修正自体は5分で完了しました。共有バリデーションユーティリティのわずか1行を変更するだけです。私はコードをプッシュし、CIの完了を待ちました。
しかし、すぐにデプロイされる代わりに、私はCIパイプラインが回り続けるのを眺めることになりました。10分が経過し、20分、30分と過ぎていきました。私たちのモノリシックなTypeScriptリポジトリは、ユーティリティフォルダのたった1文字の変更のために、Reactのストアフロント、NestJSのAPI、管理ダッシュボード、転じて30以上の共有ライブラリすべてを再ビルドしていたのです。
修正が本番環境に届くまでに, 45分もの時間が失われました。一刻を争う状況において、この遅延は単なる苛立ち以上のものです。それは多大な損失を意味します。これが「モノレポ税」です。コードベースが成長するにつれ、ビルド時間は通常線形に増加し、最終的にはチームの勢いを削いでしまいます。
根本原因:「すべてをビルドする」という誤謬
多くのチームは、Lernaや基本的なnpmワークスペースを使用してフォルダをまとめることでモノレポを開始します。通常、ビルドスクリプトは "build": "npm run build --workspaces" のような大雑把なものです。これはアプリが2つのうちは問題ありませんが、スケールした瞬間に破綻します。
本当の問題は、依存関係の把握(dependency awareness)の欠如です。ほとんどのビルドシステムは、リポジトリをプロジェクトのフラットなリストとして扱います。それらが実際には「有向非巡回グラフ(DAG)」であることを認識していません。システムが「App AはLib Bに依存しているが、App Cは全く無関係である」ということを知らなければ、最も安全で最も遅いルート、つまり「すべてを再ビルドする」という選択肢をデフォルトにしてしまいます。
シニアエンジニアからアーキテクトへとステップアップするには、視点を変える必要があります。コードだけに集中するのではなく、それを届けるためのインフラストラクチャの最適化を始めなければなりません。
Nxがアーキテクチャをどのようにマッピングするか
Nxは単なるタスクランナーではありません。プロジェクト間の関係を理解するように設計された、スマートなビルドシステムです。Turborepoのようなツールもスピードで人気がありますが、NxはマルチフレームワークのTypeScriptプロジェクトに対してより深いエコシステムを提供します。React、Angular、Node.jsを混在させながら、厳格で検索可能な依存関係グラフを維持することができます。
複雑性の可視化
最適化を始める前に、スパゲッティ状態を把握する必要があります。Nxには、プロジェクトグラフを可視化するためのツールが組み込まれています。
npx nx graph
このコマンドを実行すると、アーキテクチャ全体のインタラクティブなマップが開きます。どのライブラリが密結合で、どれが独立しているかを正確に確認できます。このグラフは、Nxが何をテストする必要があり、何を無視できるかを判断するための「脳」として機能します。
戦略1:変更があった箇所のみをビルドする
ビルドを高速化する最善の方法は、ビルドを実行しないことです。ここで nx affected の出番です。すべてのプロジェクトに対してタスクを実行する代わりに、Nxは現在のgitブランチをベース(main など)と比較します。そして、必要最小限の作業量を計算します。
もし libs/shared-ui の関数を修正した場合、Nxはグラフを確認します。そのライブラリを storefront アプリだけがインポートしているなら、Nxは backend-api と admin-panel のビルドを完全にスキップします。
CIワークフローの最適化
GitHub Actions、GitLab、Jenkinsのどれを使用していても、汎用的なビルドコマンドの使用をやめ、CI設定を更新しましょう。affected構文に切り替えます。
# 避けるべき方法: npx nx run-many -t build
# 推奨される方法: PRの影響を受けるプロジェクトのみをビルド
npx nx affected -t build --base=origin/main
これを導入した際、平均的なCI時間は45分から約12分に短縮されました。これは開発者の生産性において大きな勝利です。特に、GitHub ActionsなどのCIツールを効率的に活用することは、現代の開発において不可欠です。
戦略2:計算結果のキャッシュ
affectedコマンドを使用しても、変更されていないコードを再ビルドしてしまうことがよくあります。以前のブランチに戻った場合や、チームメイトがすでに全く同じバージョンのライブラリをビルドしている場合などです。Nxは、コンテンツアドレス指定ハッシュアルゴリズムを使用してこれを解決します。
ソースファイル、環境変数、ツールのバージョンに基づいてハッシュを作成します。そのハッシュが以前の実行と一致する場合、Nxは単にキャッシュから結果を取得します。すべてのタスクが「キャッシュヒット」してフルビルドが200ミリ秒で終わる様子は、まるで魔法のようです。
ローカルキャッシュの問題点
デフォルトでは、このキャッシュはローカルマシンの node_modules/.cache/nx に保存されます。これは個人の役には立ちますが、チーム全体の役には立ちません。CIランナーが新しいジョブを開始するたびに、履歴がゼロの「コールド」な環境になります。これを解決するには、リモートキャッシュ(Remote Caching)が必要です。
Nx Cloudによるキャッシュの共有
Nx Cloudを使用すると、組織全体で単一のキャッシュを共有できます。CIが main ブランチをビルドすると、成果物がアップロードされます。あなたが最新のコードを手元のPCにプルしてビルドを実行すると、マシンはビルド済みのファイルを数秒でダウンロードします。本質的に、CIがすでに行った作業を再利用していることになります。
セットアップするには、以下を実行します:
npx nx connect-to-nx-cloud
これにより、nx.json に accessToken が追加されます。これで、すべてのビルド成果物がクラウドに保存されるようになります。会社全体で完全に同期された、共有の dist フォルダのようなものです。
マルチフレームワークプロジェクトの管理
大規模なモノレポでは、プロジェクトごとに異なるビルド要件に苦労することがよくあります。SWC を使用する NestJS バックエンドと、Viteを使用する React フロントエンドが混在しているかもしれません。Nxはこれらをエグゼキューター(Executors)を通じて処理します。
各プロジェクトの動作は project.json ファイルで定義します:
{
"name": "api-gateway",
"targets": {
"build": {
"executor": "@nx/js:swc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/api-gateway",
"main": "apps/api-gateway/src/main.ts"
}
}
}
}
ビルドロジックを抽象化することで、Nxはキャッシュメカニズムの一貫性を保証します。Webpack、Rollup、Vitestのどれを使用していても、ロジックは同じです。「入力が変わっていなければ、キャッシュから出力を取得する」というものです。
高度なCI:分散タスク実行 (DTE)
最高のパフォーマンスに到達するには、CIが「グラフを認識」している必要があります。よくある間違いは、タスクを単一のシリアルなブロックで実行することです。代わりに、分散タスク実行 (DTE) を使用します。
DTEは複数のCIエージェントを調整します。1台のマシンが10個のビルドに苦労する代わりに、Nx CloudはエージェントAに Lib 1 をビルドさせ、エージェントBに Lib 2 をビルドさせます。エージェントCは、依存関係が整った瞬間に App 1 を開始します。これは単なる並列化ではなく、インテリジェントなオーケストレーションです。
GitHub Actionsの例
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: nrwl/nx-set-shas@v4
- run: npm ci
- run: npx nx-cloud start-agent
- run: npx nx affected -t build lint --parallel=3
結果:45分から4分へ
プロジェクトの境界を厳格にし、リモートキャッシュを有効にした後、結果は劇的に変わりました。以前は45分かかっていた午前2時のホットフィックスが、今では5分足らずでデプロイされます。その時間のほとんどは、GitHub Actionsが仮想マシンを起動するオーバーヘッドにすぎません。
チームの士気への影響も同様に大きいです。ビルドが速くなると、開発者はより小さな単位でコードをコミットし、より頻繁にテストを実行するようになります。「壊れたCI」に対する恐怖は完全に消え去りました。
成功のためのベストプラクティス
- きめ細やかなライブラリ分割: ライブラリを小さく保つほど、
nx affectedコマンドはより効果的になります。「utils」フォルダを作るのではなく、validation-utilsライブラリを作りましょう。 - 境界の強制: Nxのタグを使用して循環参照を防ぎます。UIライブラリがデータベースモデルをインポートし始めると、ビルドグラフがボトルネックになります。
- キャッシュの自動化: Nx CloudやS3バケットなどのリモートキャッシュプロバイダーを使用してください。ローカルのみのキャッシュは解決策の半分にすぎません。
- グラフの監査: 月に一度は
nx graphを実行しましょう。すべてのプロジェクトが依存している「巨大な共通ライブラリ(God Library)」を探してください。50のアプリが使用しているライブラリを修正すると、50のビルドがトリガーされてしまいます。
TypeScriptモノレポのスケーリングは、依存関係をクリーンに保つための継続的な努力です。しかし、プロジェクトグラフをしっかりと理解し、適切なキャッシュ戦略を立てれば、コードベースが数百万行に成長しても、デプロイ速度を高く維持することができます。

