Startup time, peak throughput and deployment size all depend on how your code is compiled.
C# compiles to IL in assemblies. At runtime the JIT (RyuJIT) compiles each method to native code on first call.
Tiered compilation balances startup and speed:
- Tier 0: a quick, unoptimized compile so the app starts fast.
- Methods called often (30 calls by default) or loops running long (via on-stack replacement) get recompiled at tier 1 with full optimizations.
- Dynamic PGO (on by default since .NET 8) instruments tier-0 code, then uses the observed types and branches to devirtualize interface calls, inline hot paths and lay out code better.
The JIT does inlining, bounds-check elimination, loop optimizations, struct promotion, escape analysis for some allocations (expanded in .NET 10), and vectorization through Vector128/256/512.
Ahead-of-time options:
- ReadyToRun (
PublishReadyToRun) precompiles IL into the assembly to cut startup; the JIT can still rejit hot code. - Native AOT (
PublishAot) compiles the whole app to a single native executable with no JIT: very fast startup, smaller memory, no runtime install. Costs: no dynamic code loading orReflection.Emit, limited reflection, and everything must be trimmable. The compiler emits warnings (IL2xxx, IL3xxx) for unsafe patterns.
Trimming removes unused code; code that uses reflection must annotate with [DynamicallyAccessedMembers] or use source generators.
Benchmark with BenchmarkDotNet, which handles warm-up and tiering correctly.
Example
<!-- App.csproj: options for how the code is compiled -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<!-- Native AOT: single native binary, no JIT (dotnet publish -r linux-x64) -->
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<!-- Alternatives for JIT-based apps:
<PublishReadyToRun>true</PublishReadyToRun>
<TieredPGO>true</TieredPGO> default since .NET 8
<TieredCompilation>false</TieredCompilation> fully optimize up front -->
<!-- treat trim/AOT warnings as errors so problems surface at build -->
<WarningsAsErrors>$(WarningsAsErrors);IL2026;IL3050</WarningsAsErrors>
</PropertyGroup>
</Project>Remember
- Tier 0 compiles fast; hot methods are recompiled optimized at tier 1
- Dynamic PGO uses runtime profiles to devirtualize and inline
- ReadyToRun speeds startup; Native AOT removes the JIT entirely
- Native AOT requires trimmable, reflection-light code
Watch out for
- Micro-benchmarking with a Stopwatch over a few iterations, measuring tier-0 code.