Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Establish build performance baselines and apply systematic optimization techniques. USE FOR: diagnosing slow builds, establishing before/after measurements (cold, warm, no-op scenarios), applying optimization strategies like MSBuild Server, static graph builds, artifacts output, and dependency graph trimming. Start here before diving into build-perf-diagnostics, incremental-build, or build-parallelism. DO NOT USE FOR: non-MSBuild build systems, detailed bottleneck analysis (use build-perf-diagno
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 54% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 60% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 92% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 72% | 0% |
Before optimizing a build, you need a baseline. Without measurements, optimization is guesswork. This skill covers how to establish baselines and apply systematic optimization techniques.
Related skills:
build-perf-diagnostics — binlog-based bottleneck identificationincremental-build — Inputs/Outputs and up-to-date checksbuild-parallelism — parallel and graph build tuningeval-performance — glob and import chain optimizationMeasure three scenarios to understand where time is spent:
No previous build output exists. Measures the full end-to-end time including restore, compilation, and all targets.
bash# Clean everything first dotnet clean # Remove bin/obj to truly start fresh Get-ChildItem -Recurse -Directory -Include bin,obj | Remove-Item -Recurse -Force # OR on Linux/macOS: # find . -type d \( -name bin -o -name obj \) -exec rm -rf {} + # Measure cold build dotnet build /bl:cold-build.binlog -m
Build output exists, some files have changed. Measures how well incremental build works.
bash# Build once to populate outputs dotnet build -m # Make a small change (touch one .cs file) # Then rebuild dotnet build /bl:warm-build.binlog -m
Build output exists, nothing has changed. This should be nearly instant. If it's slow, incremental build is broken.
bash# Build once to populate outputs dotnet build -m # Rebuild immediately without changes dotnet build /bl:noop-build.binlog -m
| Scenario | Expected Behavior | |----------|------------------| | Cold build | Full compilation, all targets run. This is your absolute baseline | | Warm build | Only changed projects recompile. Time proportional to change scope | | No-op build | < 5 seconds for small repos, < 30 seconds for large repos. All compilation targets should report "Skipping target — all outputs up-to-date" |
Red flags:
incremental-build skill)Record baselines in a structured way before and after optimization:
| Scenario | Before | After | Improvement |
|-------------|---------|---------|-------------|
| Cold build | 2m 15s | | |
| Warm build | 1m 40s | | |
| No-op build | 45s | | |The MSBuild server keeps the build process alive between invocations, avoiding JIT compilation and assembly loading overhead on every build.
bash# Enabled by default in .NET 8+ but can be forced dotnet build /p:UseSharedCompilation=true
The MSBuild server is started automatically and reused across builds. The compiler server (VBCSCompiler / dotnet build-server) is separate but complementary.
bash# Check if the server is running dotnet build-server status # Shut down all build servers (useful when debugging) dotnet build-server shutdown
Restart after:
bashdotnet build-server shutdown dotnet build
The UseArtifactsOutput feature (introduced in .NET 8) changes the output directory structure to avoid bin/obj clash issues and enable better caching.
xml<!-- Directory.Build.props --> <PropertyGroup> <UseArtifactsOutput>true</UseArtifactsOutput> </PropertyGroup>
# Traditional layout (before)
src/
MyLib/
bin/Debug/net8.0/MyLib.dll
obj/Debug/net8.0/...
MyApp/
bin/Debug/net8.0/MyApp.dll
# Artifacts layout (after)
artifacts/
bin/MyLib/debug/MyLib.dll
bin/MyApp/debug/MyApp.dll
obj/MyLib/debug/...
obj/MyApp/debug/...artifacts/ directory to cache/restore in CIartifacts/xml<!-- Change the artifacts root --> <PropertyGroup> <ArtifactsPath>$(MSBuildThisFileDirectory)output</ArtifactsPath> </PropertyGroup>
Deterministic builds produce byte-for-byte identical output given the same inputs. This is essential for build caching and reproducibility.
xml<!-- Directory.Build.props --> <PropertyGroup> <!-- Enabled by default in .NET SDK projects since SDK 2.0+ --> <Deterministic>true</Deterministic> <!-- For full reproducibility, also set: --> <ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild> </PropertyGroup>
Reducing unnecessary project references shortens the critical path and reduces what gets built.
bash# Visualize the dependency graph dotnet build /bl:graph.binlog # In the binlog, check project references and build times # Look for projects that are referenced but could be trimmed
xml<!-- BAD: Utils is already referenced transitively via Core --> <ItemGroup> <ProjectReference Include="..\Core\Core.csproj" /> <ProjectReference Include="..\Utils\Utils.csproj" /> </ItemGroup> <!-- GOOD: Let transitive references flow automatically --> <ItemGroup> <ProjectReference Include="..\Core\Core.csproj" /> </ItemGroup>
When you need a project to build before yours but don't need its assembly output:
xml<!-- Only ensures build order, doesn't reference the output assembly --> <ProjectReference Include="..\CodeGen\CodeGen.csproj" ReferenceOutputAssembly="false" />
When a dependency is an internal implementation detail that shouldn't flow to consumers:
xml<!-- Don't expose this dependency transitively --> <ProjectReference Include="..\InternalHelpers\InternalHelpers.csproj" PrivateAssets="all" />
For explicit-only dependency management (extreme measure for very large repos):
xml<PropertyGroup> <DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences> </PropertyGroup>
Caution: This requires all dependencies to be listed explicitly. Only use in large repos where transitive closure is causing excessive rebuilds.
/graph)Static graph mode evaluates the entire project graph before building, enabling better scheduling and isolation.
bash# Single invocation dotnet build /graph # With binary log for analysis dotnet build /graph /bl:graph-build.binlog
| Scenario | Recommendation | |----------|---------------| | Large multi-project solution (20+ projects) | ✅ Try /graph — may see significant parallelism gains | | Small solution (< 5 projects) | ❌ Overhead of graph evaluation outweighs benefits | | CI builds | ✅ Graph builds are more predictable and parallelizable | | Local development | ⚠️ Test both — may or may not help depending on project structure |
Graph build requires that all ProjectReference items are statically determinable (no dynamic references computed in targets). If graph build fails:
error MSB4260: Project reference "..." could not be resolved with static graph.Fix: Ensure all ProjectReference items are declared in <ItemGroup> outside of targets (not dynamically computed inside <Target> blocks).
bash# Use all available cores (default in dotnet build) dotnet build -m # Specify explicit core count (useful for CI with shared agents) dotnet build -m:4 # MSBuild.exe syntax msbuild /m:8 MySolution.sln
In a binlog, look for:
Use grep 'Target Performance Summary' -A 30 full.log in binlog analysis to see build node utilization.
The critical path is the longest chain of dependent projects. To shorten it:
ReferenceOutputAssembly="false" for build-order-only dependenciesbash# In CI, restore once then build without restore dotnet restore dotnet build --no-restore -m dotnet test --no-build
bash# Skip building documentation dotnet build /p:GenerateDocumentationFile=false # Skip analyzers during development (not for CI!) dotnet build /p:RunAnalyzers=false
bash# Build only the project you're working on (and its dependencies) dotnet build src/MyApp/MyApp.csproj # Don't build the entire solution if you only need one project
Always start with a binlog:
bashdotnet build /bl:perf.binlog -m
Then use the build-perf-diagnostics skill and binlog tools for systematic bottleneck identification.
Is your no-op build slow (> 10s per project)?
├── YES → See `incremental-build` skill (fix Inputs/Outputs)
└── NO
Is your cold build slow?
├── YES
│ Is restore slow?
│ ├── YES → Optimize NuGet restore (use lock files, configure local cache)
│ └── NO
│ Is compilation slow?
│ ├── YES
│ │ Are analyzers/generators slow?
│ │ ├── YES → See `build-perf-diagnostics` skill
│ │ └── NO → Check parallelism, graph build, critical path (this skill + `build-parallelism`)
│ └── NO → Check custom targets (binlog analysis via `build-perf-diagnostics`)
└── NO
Is your warm build slow?
├── YES → Projects rebuilding unnecessarily → check `incremental-build` skill
└── NO → Build is healthy! Consider graph build or UseArtifactsOutput for further gainsOther measured skills in the registry, with their headline benchmark lift.