| name | performance |
| description | Optimize application performance through async patterns, caching strategies, database optimization, profiling, and resource management. |
Performance
Purpose: Optimize application speed, throughput, and resource usage for production loads.
Strategy: Profile first, optimize bottlenecks, measure impact.
Quick Wins
| Optimization | Impact | Effort |
|---|
| Enable Response Compression | 70-90% size reduction | Low |
| Add Database Indexes | 10-100x query speed | Low |
| Implement Caching | 50-99% latency reduction | Medium |
| Use Async/Await | 5-10x throughput | Medium |
| Fix N+1 Queries | 10-1000x DB performance | Medium |
Profiling
Rule: Always profile before optimizing. Gut feelings lie.
dotnet trace collect --process-id <PID>
dotnet-counters monitor --process-id <PID>
dotnet-gcdump collect --process-id <PID>
Tools:
- Visual Studio Profiler
- JetBrains dotTrace / dotMemory
- BenchmarkDotNet (micro-benchmarks)
- Application Insights (production)
Database Optimization
Fix N+1 Queries
var users = await _context.Users.ToListAsync();
foreach (var user in users)
{
var posts = await _context.Posts.Where(p => p.UserId == user.Id).ToListAsync();
}
var users = await _context.Users
.Include(u => u.Posts)
.ToListAsync();
Add Indexes
modelBuilder.Entity<User>()
.HasIndex(u => u.Email)
.IsUnique();
modelBuilder.Entity<Post>()
.HasIndex(p => new { p.UserId, p.CreatedAt });
Use Projections
var users = await _context.Users.ToListAsync();
var users = await _context.Users
.Select(u => new { u.Id, u.Name, u.Email })
.ToListAsync();
Caching
In-Memory Cache
builder.Services.AddMemoryCache();
public class UserService
{
private readonly IMemoryCache _cache;
public async Task<User> GetUserAsync(int id)
{
return await _cache.GetOrCreateAsync($"user:{id}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return await _repository.GetByIdAsync(id);
});
}
}
Redis Cache
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "MyApp";
});
public class CachedService
{
private readonly IDistributedCache _cache;
public async Task<string> GetDataAsync(string key)
{
var cached = await _cache.GetStringAsync(key);
if (cached != null) return cached;
var data = await FetchDataAsync();
await _cache.SetStringAsync(key, data, new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
});
return data;
}
}
HTTP Caching
[ResponseCache(Duration = 300, VaryByQueryKeys = new[] { "id" })]
public IActionResult GetProduct(int id)
{
return Ok(_service.GetProduct(id));
}
app.UseResponseCaching();
Async/Await
public async Task<IActionResult> GetUsersAsync()
{
var users = await _service.GetUsersAsync();
return Ok(users);
}
public IActionResult GetUsers()
{
var users = _service.GetUsersAsync().Result;
return Ok(users);
}
var task1 = _service.GetUserAsync(1);
var task2 = _service.GetOrdersAsync(1);
await Task.WhenAll(task1, task2);
Memory Optimization
Use Span for Large Data
public string ProcessData(string data)
{
return data.Substring(0, 10).ToUpper().Trim();
}
public string ProcessData(ReadOnlySpan<char> data)
{
var span = data.Slice(0, 10);
return new string(span);
}
Object Pooling
using Microsoft.Extensions.ObjectPool;
var pool = ObjectPool.Create<StringBuilder>();
var sb = pool.Get();
try
{
sb.Append("data");
return sb.ToString();
}
finally
{
sb.Clear();
pool.Return(sb);
}
Response Compression
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
app.UseResponseCompression();
Pagination
public async Task<PagedResult<User>> GetUsersAsync(int page = 1, int pageSize = 20)
{
var query = _context.Users.AsQueryable();
var total = await query.CountAsync();
var users = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<User>
{
Items = users,
Page = page,
PageSize = pageSize,
TotalCount = total
};
}
Background Processing
builder.Services.AddHangfire(config =>
config.UsePostgreSqlStorage(connectionString));
builder.Services.AddHangfireServer();
BackgroundJob.Enqueue(() => ProcessLargeFile(fileId));
RecurringJob.AddOrUpdate("cleanup", () => CleanupOldData(), Cron.Daily);
Load Testing
ab -n 10000 -c 100 http://localhost:5000/api/users
k6 run --vus 100 --duration 30s loadtest.js
artillery quick --count 100 --num 1000 http://localhost:5000/api/users
Best Practices
✅ DO
- Profile before optimizing
- Cache frequently accessed data
- Use async/await consistently
- Add database indexes
- Paginate large datasets
- Enable response compression
- Use connection pooling
- Monitor performance in production
- Set cache expiration times
- Use CDN for static assets
❌ DON'T
- Optimize without profiling
- Cache everything indefinitely
- Mix sync and async code
- Load entire tables
- Return unlimited results
- Block async operations
- Create new DB connections per request
- Ignore memory leaks
- Skip load testing
- Optimize prematurely
Performance Targets
| Metric | Target | Critical |
|---|
| API Response Time | <200ms | <500ms |
| Database Query | <50ms | <200ms |
| Page Load (FCP) | <1.5s | <3s |
| Memory Usage | <512MB | <1GB |
| CPU Usage | <70% | <90% |
Monitoring
builder.Services.AddApplicationInsightsTelemetry();
var telemetry = services.GetRequiredService<TelemetryClient>();
telemetry.TrackMetric("ProcessingTime", duration.TotalMilliseconds);
telemetry.TrackDependency("Database", "Query", startTime, duration, success);
See Also: 06-database.md • 07-scalability.md • 15-logging-monitoring.md
Last Updated: January 13, 2026