Performance is often the deciding factor between a good and great .NET application. Below are proven techniques to extract maximum efficiency from your code, runtime, and deployment environment.
1. Use Span<T> and Memory<T> for Low‑Allocation Code
These structs allow you to work with slices of memory without heap allocations.
public static int SumNumbers(ReadOnlySpan<int> numbers)
{
int sum = 0;
foreach (var n in numbers)
sum += n;
return sum;
}
// Usage
int[] data = {1,2,3,4,5};
int result = SumNumbers(data); // No allocation
2. Leverage the ValueTask Type
When an async method often completes synchronously, returning ValueTask avoids unnecessary allocations.
public ValueTask<int> GetCachedValueAsync()
{
if (_cache.TryGetValue(out int value))
return new ValueTask<int>(value); // Synchronous path
return new ValueTask<int>(FetchFromDbAsync());
}
3. Enable Tiered Compilation
Let the JIT compile methods in two phases: a quick baseline followed by optimized code after enough executions.
// In your .runtimeconfig.json
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true,
"System.Threading.ThreadPool.MinThreads": 4,
"System.Runtime.TieredCompilation": true
}
}
}
4. Profile with PerfView or dotnet-trace
Identify hot paths and allocation sources before applying optimizations.
# Collect a trace
dotnet-trace collect --process-id <pid> -o trace.nettrace
# Analyze with PerfView
PerfView.exe trace.nettrace
5. Reduce Boxing & Unboxing
Prefer generic collections and structs over object when possible.
6. Use Asynchronous Streams (IAsyncEnumerable)
Stream data without buffering the entire collection.
public async IAsyncEnumerable<Record> GetRecordsAsync()
{
await using var cmd = new SqlCommand("SELECT * FROM LargeTable", connection);
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
yield return Map(reader);
}
7. Optimize Startup Time
- Trim unused assemblies with
PublishTrimmed. - ReadyToRun (R2R) compilation for faster first‑run performance.
- Use
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishReadyToRun=true.
8. Cache Frequently Used Results
Use MemoryCache or distributed caches (Redis) for expensive calculations.
private static readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
public async Task<string> GetUserProfileAsync(Guid userId)
{
if (_cache.TryGetValue(userId, out string profile))
return profile;
profile = await _db.GetUserProfileAsync(userId);
_cache.Set(userId, profile, TimeSpan.FromMinutes(10));
return profile;
}
9. Avoid Synchronous I/O
Blocking I/O can stall thread pool threads, reducing throughput.
10. Monitor Production Metrics
Implement OpenTelemetry and export metrics to Prometheus or Azure Monitor.
Applying these techniques will typically yield a 15‑40% performance gain depending on the workload. Always measure before and after each change.