大規模TypeScriptモノレポのビルドと依存関係管理の高速化
Emily Parker
Product Engineer · Leapcell

はじめに
ウェブ開発の進化する状況において、モノレポは複雑なフルスタックアプリケーションを管理するためのアーキテクチャとしてますます人気が高まっています。コード共有の簡素化、ツールの統一、デプロイの効率化といった、否定できないメリットを提供します。しかし、これらのモノレポがスケールするにつれて、特にTypeScriptで構築されたものでは、開発者はしばしば深刻な課題に直面します。それは、ビルド時間の遅延と複雑な依存関係管理です。TypeScriptの大規模なコードベースは、静的型付けとコンパイル要件に加え、複数の相互依存するアプリケーションやライブラリをしばしば含むフルスタックアーキテクチャと相まって、フィードバックループの長い開発体験にすぐに悩まされるようになります。これは開発者を悩ませるだけでなく、アジリティとデリバリー速度を低下させます。この記事では、これらのハードルを克服するための実践的な戦略を探求し、さまざまなツールを実証して、遅くて扱いにくいモノレポを効率的で楽しく作業できる開発環境に変革します。
コアコンセプトの理解
最適化技術に飛び込む前に、この議論の中心となる主要な概念について共通の理解を確立しましょう。
モノレポ
モノレポは、複数の明確に区別されたプロジェクトを含む単一のリポジトリであり、しばしば相互に関連するコードを持っています。フルスタックのコンテキストでは、これにはフロントエンドアプリケーション、バックエンドAPI、共有UIコンポーネント、ユーティリティライブラリなどが含まれる可能性があり、すべて同じGitリポジトリに存在します。
TypeScript
TypeScriptはJavaScriptの強く型付けされたスーパーセットであり、プレーンなJavaScriptにコンパイルされます。静的型チェックを通じてコードの品質と保守性を大幅に向上させますが、このコンパイルステップは、特に大規模なプロジェクトではビルドプロセスにオーバーヘッドを追加します。
ビルド速度
これは、ソースコード(TypeScript、JSXなど)をデプロイ可能な成果物(JavaScript、CSS、しばしばバンドルおよびミニファイされたもの)に変換するのにかかる時間です。モノレポでは、「ビルド」とは通常、複数のプロジェクトとその依存関係のコンパイルを伴います。
依存関係管理
これは、モノレポ内のパッケージが互いにどのように関連しているか、外部のサードパーティライブラリがどのように処理されるか、そしてこれらの関係がビルドプロセスにどのように影響するかを包含します。npm、Yarn、pnpmのようなツールがこれに使用されます。
モノレポ最適化の柱
大規模なTypeScriptフルスタックモノレポの最適化は、一般的にいくつかの主要な原則に基づいています。並列処理、キャッシング、増分ビルド、効率的な依存関係グラフです。
1. 依存関係グラフのためのワークスペースの活用
最新のパッケージマネージャーは、モノレポに不可欠な「ワークスペース」機能を提供しています。ワークスペースを使用すると、単一のルートリポジトリ内で複数のパッケージを管理し、パッケージ間の依存関係を処理し、共通の外部依存関係をルートnode_modules
ディレクトリにホイストして、スペースを節約し、インストール時間を短縮できます。
モノレポ構造を検討してください。
my-monorepo/
├── packages/
│ ├── frontend/
│ │ ├── src/
│ │ └── package.json
│ ├── backend/
│ │ ├── src/
│ │ └── package.json
│ └── shared-ui/
│ ├── src/
│ └── package.json
└── package.json
ルートのpackage.json
はワークスペースを定義します。
// my-monorepo/package.json { "name": "my-monorepo-root", "private": true, "workspaces": [ "packages/*" ], "scripts": { "build": "turbo run build" // TurboRepoのようなモノレポツールを使用 } }
各パッケージのpackage.json
は、その内部依存関係を宣言します。
// my-monorepo/packages/frontend/package.json { "name": "frontend", "version": "1.0.0", "dependencies": { "shared-ui": "workspace:*" // 内部パッケージを参照 } }
このセットアップにより、パッケージマネージャーは正確な依存関係グラフを構築でき、モノレポツールはそれを活用できます。
2. モノレポ対応ビルドツール
npm run build
のような汎用タスクランナーは、モノレポのパッケージ間依存関係やキャッシングのニーズに対応するのに苦労します。ここで専門のモノレポツールが輝きます。TurborepoやNxのようなツールは、モノレポの依存関係グラフを理解し、ビルドプロセスを最適化するように設計されています。
Turborepoを例に説明します。
- タスクグラフ: Turborepoは、他のタスクに依存するタスク(例:
build
、test
、lint
)を示すタスクグラフを構築します。たとえば、frontend#build
はshared-ui#build
に依存する可能性があります。 - インテリジェントキャッシング: ファイルの内容、
package.json
およびtsconfig.json
ファイル、さらにはnode_modules
を各タスクにハッシュします。タスクの入力が前回実行時(ローカルまたはリモートキャッシュ)から変更されていない場合、Turborepoはそのタスクをスキップし、キャッシュからその出力を復元します。 - 並列実行: お互いに依存しないタスクは並列で実行でき、全体的なビルド時間を大幅に短縮できます。
例: Turborepoの設定
まず、モノレポのルートにTurborepoをインストールします: npm install turbo --save-dev
。
次に、ルートのturbo.json
ファイルでタスクを定義します。
// turbo.json { "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"], // 依存関係の'build'に依存 "outputs": ["dist/**", ".next/**"] // これらの出力をキャッシュ }, "test": { "dependsOn": ["^build"], "outputs": [] }, "lint": { "outputs": [] }, "dev": { "cache": false, // 開発サーバーは通常キャッシュされない "persistent": true // 実行し続ける } } }
これで、turbo run build
を実行すると、Turborepoは次のことを行います。
- すべてのパッケージの
build
スクリプトの依存関係グラフを分析します。 - 可能な場合はビルドを並列化します。
- ビルド出力をキャッシュします。関連するコードが変更されていない場合、後続の実行はほぼ瞬時に行われます。
shared-ui
に変更があったと想像してください。Turborepoはshared-ui
とfrontend
(shared-ui
に依存)のみを再構築し、モノレポ全体でturbo run build
を実行してもbackend
には触れません。
3. 増分TypeScriptビルド
TypeScript自体も増分コンパイルをサポートしており、変更されたファイルや依存関係が変更されたファイルのみを再コンパイルします。これはtsconfig.json
のincremental
コンパイラオプションを介して有効になります。
// tsconfig.json (または tsconfig.build.json) { "compilerOptions": { "incremental": true, "tsBuildInfoFile": "./.tsbuildinfo", // ビルド情報が格納される場所 // ... その他のオプション } }
incremental
がtrue
の場合、TypeScriptはプロジェクトグラフとビルド状態に関する情報を格納する.tsbuildinfo
ファイルを作成します。後続のコンパイルで、tsc
はこのファイルを使用して、再コンパイルが必要なものを迅速に判断します。
これは役立ちますが、モノレポでは、モノレポツールで調整されていない場合、tsc --build --watch
または単にtsc --build
は、パッケージ間で必要以上に再構築する可能性があります。TurborepoとNxは通常tsc
の呼び出しをラップするため、それらのキャッシングメカニズムはTypeScriptの増分機能と連携して機能します。
4. 最適化されたTypeScript構成
tsconfig.json
ファイルを微調整することで、パフォーマンスが向上します。
noEmit
とemitDeclarationOnly
: 純粋に型定義(例: 共有型パッケージ)であるパッケージの場合、JS出力が不要であればnoEmit: true
を使用するか、実際のランタイムJSをコンパイルせずに.d.ts
ファイルのみを生成するにはemitDeclarationOnly: true
を使用します。references
: TypeScriptのプロジェクト参照機能により、大規模なプロジェクトをより小さなtsconfig.json
ファイルに分割できます。これにより、tsc
は影響を受けるプロジェクトのみを再コンパイルすることで、より高速な増分チェックを実行できます。これはモノレポツールと密接に統合されます。
// my-monorepo/packages/frontend/tsconfig.json { "extends": "../../tsconfig.base.json", "compilerOptions": { // ... }, "references": [ { "path": "../shared-ui" } // 内部パッケージへの参照 ] }
これにより、shared-ui
が変更されると、frontend
のtsconfig
は、新しいshared-ui
型に対して型チェックする必要があることを認識します。
5. 効率的なパッケージマネージャー
npmとYarn Classicも機能しますが、pnpmと**Yarn Plug'n'Play (PnP)**は、主にnode_modules
を最適化することで、モノレポに顕著なパフォーマンス上の利点をもたらします。
- pnpm: コンテンツアドレス指定可能なストアを使用してディスクスペースを節約し、インストールを高速化します。複数のプロジェクトが同じバージョンのパッケージに依存している場合、pnpmはそのパッケージをディスクに1回だけ保存し、個々のプロジェクトの
node_modules
フォルダーにハードリンクまたはシンボリックリンクします。これにより、npm install
が大幅に高速化され、ディスクスペースの消費が削減されます。
pnpmを使用するには、モノレポのルートでnpm install
をpnpm install
に置き換えるだけです。
- Yarn PnP: パッケージ名をその正確な場所にマッピングする
.pnp.cjs
ファイルを生成することで、node_modules
の「ホイスト問題」を解決します。これにより、多くの場合、物理的なnode_modules
ディレクトリが不要になり、インストールと依存関係の解決がさらに高速化され、信頼性が向上します。
6. リモートキャッシング
チームにとっては、ローカルキャッシングは良いですが、リモートキャッシングは変革的です。Turborepoのようなツールは、キャッシュアーティファクトを共有リモートキャッシュ(例: Vercelのリモートキャッシュ、AWS S3、またはカスタムHTTPサーバー)にアップロードおよびダウンロードできます。
これにより、チームメンバー(またはCI/CDパイプライン)がプロジェクトをビルドした場合、チームの他のメンバーまたはCI/CDでの後続のビルドも、クリーンな状態から開始した場合でも、そのキャッシュからメリットが得られます。これにより、CIビルド時間が数分から数秒に短縮される可能性があります。
Turborepoでリモートキャッシングを設定するには、通常、キャッシングプロバイダーの資格情報を使用して環境変数または~/.config/turborepo/config.json
ファイルをセットアップする必要があります。
結論
大規模なTypeScriptフルスタックモノレポの最適化には、多角的なアプローチが必要です。インテリジェントなツールと慎重な設定を組み合わせる必要があります。TurborepoまたはNxのようなモノレポ対応ビルドツールを活用し、TypeScriptの増分コンパイルとプロジェクト参照を有効にし、pnpmのような効率的なパッケージマネージャーを使用し、リモートキャッシングを採用することにより、開発チームはビルド速度を劇的に向上させ、依存関係管理を合理化できます。その結果、より生産的な開発体験、より速いフィードバックループ、そしてプロジェクトとともにスムーズにスケーリングするモノレポが得られます。これらの最適化戦略への投資は、潜在的なパフォーマンスのボトルネックを開発ワークフローの強力なアクセラレータに変えます。