.NET performance tips
Concrete practices to improve throughput and reduce latency in modern .NET applications — from object pooling to correct async usage.
Performance in .NET isn't magic: it's about measuring, understanding the bottleneck and applying the right optimization. Here are some tips I use in production.
1. Prefer ArrayPool and ObjectPool for hot allocations
In hot paths, avoiding allocations is more important than micro-optimizing algorithms. ArrayPool<T> reuses buffers and reduces GC pressure.
using System.Buffers;
public byte[] ProcessData(ReadOnlySpan<byte> input)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(input.Length);
try
{
// ...process using buffer...
input.CopyTo(buffer);
return buffer[..input.Length].ToArray();
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}2. Use async correctly
async isn't a synonym for "fast". There is overhead to a state machine. For truly synchronous and cheap operations, avoid async.
public async Task<User> GetUserAsync(int id)
{
await using var ctx = new AppDbContext();
return await ctx.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == id)
.ConfigureAwait(false);
}Use
ConfigureAwait(false)in library code to avoid capturing the synchronization context.
3. Avoid LINQ in hot loops
LINQ is beautiful, but it creates closures and extra iterations. In high-frequency loops, prefer for:
int sum = 0;
for (int i = 0; i < items.Length; i++)
{
sum += items[i].Value;
}4. Measure before optimizing
Use dotnet-counters, dotnet-trace and BenchmarkDotNet before changing anything. Optimizing without measuring is putting out an imaginary fire.
dotnet trace collect --providers Microsoft-DotNETCore-SampleProfilerConclusion
Performance is cumulative: every allocation avoided, every ConfigureAwait(false), every loop converted from LINQ to for contributes to a healthier application in production.