dotnet-build-optimization について
.NET SDKのビルド性能を診断し、改善するスキルです。SDK形式のプロジェクトを対象に、インクリメンタルビルドの失敗、バイナリログ分析、並列ビルド、ビルドキャッシュ、復元の最適化を扱います。
主な機能
- 01インクリメンタルビルド失敗の診断
- 02MSBuildバイナリログの分析
- 03並列ビルドとグラフビルドの設定
- 04NuGet復元とキャッシュの最適化
- 05ビルドが繰り返される主な原因の解説
活用シーン
- 01ソース変更なしでターゲットが再実行される原因の特定
- 02ビルドイベントとターゲット所要時間の確認
- 03大規模ソリューションのビルド性能改善
- 04CIでの依存関係復元時間の短縮
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-build-optimizationインストール後、Claude Code で/dotnet-build-optimizationを入力して読み込まれたか確認します。
詳細
dotnet-build-optimization
ビルドのパフォーマンス問題を診断し、解決するためのガイダンスです。増分ビルド失敗の診断ワークフロー、MSBuild Structured Log Viewer を使ったバイナリログの分析、並列ビルドの構成、ビルドキャッシュ、復元の最適化について説明します。毎回のビルドでフルビルドが実行される症状から、根本原因(Inputs/Outputs の欠落、タイムスタンプの破損、ジェネレーターの副作用)を特定し、修正するまでの診断ワークフローを扱います。
バージョンの前提: .NET 8.0+ SDK(MSBuild 17.8+)。すべての例で SDK スタイルのプロジェクトを使用します。
対象範囲: このスキルでは、ビルドの最適化と診断(増分ビルドの失敗、バイナリログ、並列ビルド、ビルドキャッシュ、復元の最適化)を扱います。MSBuild エラーの解釈と CI の差異の診断は [skill:dotnet-build-analysis] が担当します。MSBuild の記述(targets、props、items、conditions)は [skill:dotnet-msbuild-authoring] が担当します。カスタムタスクの開発は [skill:dotnet-msbuild-tasks] が担当します。NuGet ロックファイルと Central Package Management の構成は [skill:dotnet-project-structure] が担当します。
相互参照:[skill:dotnet-msbuild-authoring] では、カスタムターゲット、インポート順序、増分ビルドの記述パターンについて説明しています。[skill:dotnet-msbuild-tasks] では、カスタムタスクの開発について説明しています。[skill:dotnet-build-analysis] では、MSBuild エラー、NuGet の復元失敗、CI の差異の診断について説明しています。[skill:dotnet-project-structure] では、ロックファイル、CPM、nuget.config の構成について説明しています。
増分ビルド失敗の診断
ソースに変更がないのにターゲットが毎回のビルドで実行される場合、そのビルドは増分ビルドではありません。これは時間を浪費し、実際の変更を見えにくくします。診断ワークフローは、症状の検出、バイナリログの取得、問題のあるターゲットの特定、増分ビルドに失敗した理由の判定、修正の適用という、繰り返し実施できる手順に従います。
診断ワークフロー
1. Symptom: Build takes longer than expected, or output says
"Building target 'X' completely" on every build
2. Capture binary log: dotnet build /bl
3. Open the .binlog in MSBuild Structured Log Viewer
4. Search for targets that ran (not skipped)
5. Check: Does the target have Inputs/Outputs?
- No -> Add Inputs/Outputs (see fix patterns below)
- Yes -> Compare timestamps: are outputs older than inputs?
-> Check for volatile writers or missing output files
6. Apply fix, rebuild, verify target is skipped
ステップ 1:バイナリログを取得する
# Produce msbuild.binlog in the project directory
dotnet build /bl
# Named log file
dotnet build /bl:build-debug.binlog
# Binary log for restore + build (captures full pipeline)
dotnet build /bl -restore
/bl スイッチを使うと、プロパティ評価、項目リスト、ターゲットの開始と終了、タスクの実行、タイムスタンプなど、すべての MSBuild イベントがコンパクトなバイナリ形式で記録されます。バイナリログにはソースの完全なパスと環境変数が含まれるため、バージョン管理にコミットしたり、一般に公開したりしないでください。
ステップ 2:MSBuild Structured Log Viewer で開く
msbuildlog.com からダウンロードします。.binlog ファイルを開きます。主なビューは次のとおりです。
| ビュー | 用途 |
|---|---|
| Timeline | どのターゲットが並列に実行されたか、各ターゲットにどれくらい時間がかかったかを確認します |
| Target Results | "Built"(実行)と "Skipped"(増分ビルドでスキップ)でフィルターします |
| Search | 特定のターゲット名、プロパティ値、ファイルパスを検索します |
| Properties | ビルド中の任意の時点で評価されたプロパティ値を確認します |
| Items | メタデータを含む項目コレクション(Compile、Content など)を確認します |
ステップ 3:増分ビルドされないターゲットを見つける
Structured Log Viewer でターゲット名を検索し、その結果を確認します。増分ビルドされるはずなのにすべて実行されたターゲットには、理由とともに "Building target 'X' completely" と表示されます。
- "Output file does not exist" — 想定される出力ファイルが存在しないか、削除されています
- "Input file is newer than output file" — ソースファイルが変更されたか、前のステップで出力が書き換えられています
- Inputs/Outputs の宣言がない — MSBuild には最新かどうかを確認する方法がないため、ターゲットは毎回実行されます
増分ビルド失敗の一般的なパターン
カスタムターゲットでの Inputs/Outputs の欠落
症状: カスタムターゲットが毎回のビルドで実行されます。
根本原因: ターゲットに Inputs/Outputs 属性がありません。これらがないと、MSBuild は無条件にターゲットを実行します。
修正: 実際に読み書きするファイルを反映した Inputs と Outputs を追加します。
<Target Name="GenerateVersionFile" BeforeTargets="CoreCompile">
<WriteLinesToFile File="$(IntermediateOutputPath)Version.g.cs"
Lines="[assembly: System.Reflection.AssemblyInformationalVersion("$(Version)")]"
Overwrite="true" />
</Target>
<Target Name="GenerateVersionFile"
BeforeTargets="CoreCompile"
Inputs="$(MSBuildProjectFullPath)"
Outputs="$(IntermediateOutputPath)Version.g.cs">
<WriteLinesToFile File="$(IntermediateOutputPath)Version.g.cs"
Lines="[assembly: System.Reflection.AssemblyInformationalVersion("$(Version)")]"
Overwrite="true" />
</Target>
Inputs/Outputs のパターンとバッチ処理の詳細については、[skill:dotnet-msbuild-authoring] を参照してください。
ファイルコピーによるタイムスタンプの破損
症状: 出力ファイルのタイムスタンプが常に入力ファイルより新しいため、ターゲットが再実行されます。
根本原因: SkipUnchangedFiles="true" を指定していない Copy タスクは、内容が同一でも、コピーするたびにコピー先のタイムスタンプを更新します。
修正方法:
<Copy SourceFiles="@(ConfigTemplate)"
DestinationFolder="$(OutputPath)" />
<Copy SourceFiles="@(ConfigTemplate)"
DestinationFolder="$(OutputPath)"
SkipUnchangedFiles="true" />
無条件で書き込むジェネレーター
症状: 入力に変更がないにもかかわらず、コードジェネレーターのターゲットがビルドのたびに実行されます。
根本原因: ジェネレーターが出力ファイルを無条件で書き込み、内容が同一でもタイムスタンプを更新します。次のビルドでは、ジェネレーター自体が下流のターゲットの入力であるため、「入力が出力より新しい」と判断されます。
修正方法: まず一時ファイルに書き込み、内容が異なる場合にのみコピーします。
<Target Name="GenerateCode"
BeforeTargets="CoreCompile"
Inputs="@(SchemaFile)"
Outputs="@(SchemaFile->'$(IntermediateOutputPath)%(Filename).g.cs')">
<Exec Command="codegen %(SchemaFile.Identity) -o $(IntermediateOutputPath)%(SchemaFile.Filename).g.cs.tmp" />
<Copy SourceFiles="$(IntermediateOutputPath)%(SchemaFile.Filename).g.cs.tmp"
DestinationFiles="$(IntermediateOutputPath)%(SchemaFile.Filename).g.cs"
SkipUnchangedFiles="true" />
</Target>
変動する中間ファイル
症状: 中間出力に依存するターゲットが、先行するターゲットによってそれらのファイルが常に再生成されるため、再実行されます。
根本原因: 上流のターゲットが、適切な Inputs/Outputs を設定せずに中間ファイル(生成コードやリソースバンドルなど)を生成しているため、ファイルがビルドのたびに書き換えられます。下流のターゲットは、それらのファイルが「変更された」と判断して再実行されます。
修正方法: 上流のターゲットに Inputs/Outputs を追加します。上流のターゲットが SDK または NuGet パッケージに含まれていて変更できない場合は、Touch タスクを使用して、内容が変更されていないときにその出力のタイムスタンプを安定した値に戻します。
バイナリログの分析
バイナリログの取得
# Basic binary log (outputs msbuild.binlog)
dotnet build /bl
# Named output file
dotnet build /bl:diagnostic.binlog
# Include restore phase
dotnet build /bl -restore
# Detailed verbosity in console + binary log
dotnet build /bl /v:minimal
バイナリログは、/v: の詳細レベルに関係なくすべてを記録します。/v: スイッチが制御するのはコンソール出力のみです。診断には常に /bl を使用してください。コンソールの詳細レベルは、手早く確認するためのものです。
プリプロセス済みプロジェクトの表示
-pp(プリプロセス)スイッチを使用すると、すべてのインポート、条件、プロパティの置換が適用された後の、完全に評価済みのプロジェクトファイルが出力されます。
# Dump the preprocessed project to stdout
dotnet msbuild MyApp.csproj -pp
# Redirect to a file for easier reading
dotnet msbuild MyApp.csproj -pp > preprocessed.xml
プリプロセス済みの出力には、次の内容が表示されます。
- インポートされたすべての
.propsファイルと.targetsファイル、およびそれらのソースパス - 最終的に評価されたプロパティ値
- すべての Include/Exclude/Update/Remove 操作の適用後の完全な項目リスト
- 解決済みの条件を含むすべてのターゲット定義
バイナリログを開かずに「このプロパティはどこから来ているのか?」や「このターゲットを定義している .targets ファイルはどれか?」を確認するには、-pp を使用します。
バイナリログで確認する主な診断項目
| 検索クエリ | 判明する内容 |
|---|---|
ターゲット名(例:CoreCompile) | ターゲットが実行されたか、スキップされたか、およびその理由 |
$property(例:$TargetFramework) | ビルド内の各時点で評価された値 |
ファイルパス(例:Order.cs) | どのターゲットがいつそのファイルを処理したか |
"Building target" | 実行された(スキップされなかった)すべてのターゲット |
"Skipping target" | スキップされたすべてのターゲット(インクリメンタルビルドの条件に合致) |
| 警告/エラーのテキスト | 診断情報のソースの場所とビルドのコンテキスト |
並列ビルド
ソリューションレベルの並列処理
MSBuild は複数のワーカーノードを使用して、ソリューション内の依存関係のないプロジェクトを並列にビルドできます。
# Use all available CPU cores (default behavior for dotnet build)
dotnet build
# Explicit: 4 worker nodes
dotnet build /m:4
# Single-threaded (useful for debugging build order issues)
dotnet build /m:1
dotnet build はデフォルトで /m(マルチプロセス)を有効にします。各ワーカーノードは、1 つのプロジェクトを一度にビルドする独立した MSBuild プロセスです。依存関係のないプロジェクトは並列にビルドされます。
グラフビルドモード
グラフビルド(/graph)では、ビルドの前にプロジェクトの依存関係グラフを分析し、並列処理が最大になるようにプロジェクトの実行をスケジュールします。
# Graph-aware parallel build
dotnet build /graph
# Graph build with explicit parallelism
dotnet build /graph /m:8
デフォルトの並列ビルドと比較したグラフモードの利点:
- 静的スケジューリング: ビルド中に依存関係を検出するのではなく、事前に依存関係グラフ全体を特定します
- 重複する評価の回避: 各プロジェクトは、参照元のプロジェクトごとではなく、1 回だけ評価されます
- ノード利用率の向上: 依存関係が満たされ次第、ワーカーノードにプロジェクトが割り当てられます
グラフモードは、依存関係グラフに高い並列性がある大規模なソリューション(50 以上のプロジェクト)で特に効果的です。
BuildInParallel タスク属性
MSBuild タスクなどの個々の MSBuild タスクでは、並列呼び出しに対応するかどうかを宣言できます。
<MSBuild Projects="@(ProjectReference)"
BuildInParallel="true"
Targets="Build" />
BuildInParallel="true" を指定すると、MSBuild タスクはプロジェクトの一覧を、利用可能なワーカーノードに分散できます。これは、ソリューションのビルドでプロジェクトのコンパイルを並列化するために使われる仕組みです。
並列ビルドの問題を診断する
並列ビルドでは、逐次ビルドでは表面化しない潜在的な問題が現れることがあります。
- 共有ファイルの競合状態: 2 つのプロジェクトが同時に同じ出力ディレクトリへ書き込みます。対処法:プロジェクトごとに出力ディレクトリを使用します(SDK の既定値は
bin/Debug/$(TargetFramework)/です)。 - 依存関係の未宣言: プロジェクト A はプロジェクト B の出力に依存していますが、
<ProjectReference>を宣言していません。逐次ビルドでは偶然 B が先にビルドされますが、並列ビルドでは A が先にビルドされる場合があります。対処法:明示的な<ProjectReference>を追加します。 - ディレクトリ作成の競合: 複数のプロジェクトが同じ中間ディレクトリを作成します。対処法:
ContinueOnError="true"を指定したMakeDirタスクを使用するか、各プロジェクトが独自の$(IntermediateOutputPath)を使用するようにします。
/m:1 を使ってビルドが逐次実行で成功することを確認してから、/m を使って並列実行の問題がないか確認します。タイムライン表示付きのバイナリログでは、プロジェクトのスケジュールを確認でき、競合状態を特定できます。
ビルドキャッシュと復元の最適化
NuGet 復元の最適化
NuGet の復元は、特に CI では、ビルド工程の中で最も時間がかかることがよくあります。次の方法で復元時間を短縮できます。
# Locked restore: skip resolution if lock file is current
dotnet restore --locked-mode
# Use lock files for deterministic restores
dotnet restore --use-lock-file
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
ロックファイルを使った復元(--locked-mode)では、依存関係の解決アルゴリズムを完全に省略し、packages.lock.json に記載された正確なバージョンを読み込みます。これにより高速化でき、CI でもローカルでテストしたものと同じバージョンが確実に使われます。ロックファイルと CPM の設定について詳しくは、[skill:dotnet-project-structure] を参照してください。
SDK のビルドキャッシュ
.NET SDK は、冗長な作業を避けるため、複数のビルド成果物をキャッシュします。
| キャッシュ | 場所 | 用途 |
|---|---|---|
| NuGet グローバルパッケージ | ~/.nuget/packages/ | ダウンロード済みのパッケージ内容 |
| NuGet HTTP キャッシュ | ~/.local/share/NuGet/http-cache/ | フィードへのクエリに対する HTTP レスポンスのキャッシュ |
| MSBuild プロジェクト結果キャッシュ | メモリ内(ビルドセッションごと) | ビルド済みプロジェクトの再評価を省略 |
obj/ 中間出力 | プロジェクトごとの obj/ ディレクトリ | コンパイラーの状態、生成ファイル、タイムスタンプ |
CI ビルドの最適化
# GitHub Actions: cache NuGet packages between runs
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/packages.lock.json') }}
restore-keys: |
nuget-${{ runner.os }}-
# Use locked restore for speed and determinism
- name: Restore
run: dotnet restore --locked-mode
NoWarn と TreatWarningsAsErrors の方針
アナライザーが関与する場合、ビルドレベルの警告設定がビルド時間に影響します。
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NoWarn>$(NoWarn);CA2007</NoWarn>
</PropertyGroup>
警告設定のルール:
Directory.Build.propsでTreatWarningsAsErrorsを有効にし、ローカルビルドと CI ビルドの動作を同じにしますNoWarnは必要な場合に限って使い、必ずインラインの理由コメントを付けます- ルールごとの制御には、
NoWarnより.editorconfigの重大度ルールを優先します - 警告抑制の誤用を検出する方法については、[skill:dotnet-build-analysis] を参照してください
エージェントが陥りやすい落とし穴
-
ビルドの問題を診断する際に、
/blを指定せずdotnet buildを実行すること。 既定の詳細度でのコンソール出力には、ターゲットが実行された理由を示す重要な情報が含まれません。診断時は必ずバイナリログ(/bl)を記録してください。コンソールの詳細度にかかわらず、すべての情報が記録されます。 -
Inputs/Outputsがなくてもインクリメンタルビルドが機能すると考えること。Inputs/Outputsのないターゲットは、ビルドのたびに必ず実行されます。MSBuild には暗黙のインクリメンタルビルド機能はありません。ターゲットが読み取るファイルと書き込むファイルを宣言する必要があります。詳しいパターンについては、[skill:dotnet-msbuild-authoring] を参照してください。 -
Copy タスクで
SkipUnchangedFiles="true"を指定し忘れること。 このフラグがないと、Copyは常にコピー先のタイムスタンプを更新します。その結果、ファイルの内容が同一でも、後続のターゲットが再実行されます。 -
ビルドの調査に
/blではなく/v:diagnosticを使うこと。 診断レベルの詳細度では、何千行もの出力がコンソールに表示され、検索も困難です。バイナリログには同じ情報が構造化された検索可能な形式で記録されます。/blと Structured Log Viewer を使用してください。 -
.binlogファイルを確認せずに共有すること。 バイナリログには、ファイルのフルパス、環境変数の値、MSBuild プロパティ経由で渡された秘密情報などが含まれる可能性があります。外部と共有する前に内容を確認するか、機密情報を取り除いてください。 -
/m(並列ビルド)は常に高速だと思い込むこと。 小規模なソリューション(プロジェクトが 5 個未満)では、ワーカーノードの起動によるオーバーヘッドが並列化のメリットを上回る場合があります。/mの有無でプロファイルを取得して確認してください。大規模なソリューションでは、/graphモードのほうが既定の/mよりも適切にスケジューリングできます。 -
CI で
--locked-modeを使わずにpackages.lock.jsonをコミットすること。 ロックファイルが役立つのは、CI でロックモードの復元を行う場合に限られます。--locked-modeを指定しないと、NuGet はロックファイルを無視して通常どおり依存関係を解決するため、決定論的な復元という目的が損なわれます。 -
バイナリログを確認せずに、ビルドパフォーマンスを改善するため
.csprojのプロパティを変更すること。 「ビルドが遅い」問題の多くは、グローバルなビルド構成ではなく、インクリメンタルビルドに対応していない単一のターゲットが原因です。広範囲な構成変更を行う前に、/blで診断してください。