Je API-endpoint doet er drie seconden over. Je hebt al een uur naar de code gestaard. De profiler wijst naar de query-laag, maar je ziet het niet. Alles ziet er correct uit — de LINQ is clean, de repository geeft de juiste data terug, de mapping is simpel.
Je gooit de code naar Claude Code met de vraag: “Waarom is dit traag?” Tien seconden later wijst het naar een N+1 query die je compleet over het hoofd zag.
Dat is geen fantasie. Dat is mijn dinsdagmiddag.
Het N+1 query-probleem
Dit is verreweg het meest voorkomende performanceprobleem dat Claude Code vindt in .NET-projecten. En het is verraderlijk, want de code ziet er volkomen normaal uit.
public async Task<List<OrderDto>> GetRecentOrders(int customerId)
{
var orders = await _context.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync();
return orders.Select(o => new OrderDto
{
Id = o.Id,
Total = o.Total,
ProductName = o.Product.Name // lazy load per order
}).ToList();
}
Eén query voor de orders. Daarna één query per order om het product op te halen. Tien orders? Elf queries. Honderd orders? Honderdeen queries. Je database huilt, en je weet niet waarom.
Claude Code ziet dit vrijwel meteen. Het stelt de fix voor die je verwacht:
var orders = await _context.Orders
.Include(o => o.Product)
.Where(o => o.CustomerId == customerId)
.ToListAsync();
Eén query. Klaar. Maar het gaat verder dan de quick fix — Claude legt uit waarom lazy loading hier toeslaat en waarom .Include() het oplost. Dat is het verschil met een linter die alleen “mogelijke N+1” roept.
LINQ die er clean uitziet maar het niet is
LINQ is prachtig. Het leest als Engels, het is composable, en het verbergt complexiteit. Dat laatste is ook het probleem.
var activeUsers = await _context.Users
.ToListAsync(); // haalt ALLE users op
var filtered = activeUsers
.Where(u => u.IsActive)
.OrderBy(u => u.LastLogin)
.Take(10)
.ToList();
Die .ToListAsync() op regel twee haalt je volledige gebruikerstabel in geheugen. Filtering, sortering en paginatie gebeuren in C# in plaats van in SQL. Met duizend gebruikers merk je het niet. Met honderdduizend wel.
Claude Code vangt dit patroon consistent. Het verplaatst de filters vóór de materialisatie:
var filtered = await _context.Users
.Where(u => u.IsActive)
.OrderBy(u => u.LastLogin)
.Take(10)
.ToListAsync();
Eén query, tien rijen over de draad. De database doet waar die goed in is.
Andere LINQ-valkuilen die Claude regelmatig vlagt: meerdere enumeraties van dezelfde IEnumerable, .OrderBy() gevolgd door .OrderBy() (in plaats van .ThenBy()), en .Count() > 0 waar .Any() had gemoeten.
Async anti-patronen
Dit zijn de stille killers. Code die compileert, tests die slagen, en toch loopt je applicatie vast onder load.
public OrderDto GetOrder(int id)
{
// blokkeert een thread pool thread
var order = _orderService.GetOrderAsync(id).Result;
return MapToDto(order);
}
.Result en .Wait() blokkeren de huidige thread terwijl ze wachten op het async resultaat. In een ASP.NET Core-applicatie betekent dat: je thread pool raakt uitgeput onder load. Requests gaan in de wachtrij staan. Response times schieten omhoog. En het lastige is: met weinig verkeer merk je er niets van.
Claude Code vlagt dit direct en stelt de async-all-the-way oplossing voor:
public async Task<OrderDto> GetOrderAsync(int id)
{
var order = await _orderService.GetOrderAsync(id);
return MapToDto(order);
}
Een ander anti-patroon dat Claude goed herkent: Task.Run() in ASP.NET Core-controllers. Dat voegt een thread pool switch toe zonder voordeel — je hebt al een thread pool thread, je hoeft er geen nieuwe op te starten.
Geheugen en allocaties
Geheugendruk is lastiger te spotten dan trage queries. Je applicatie draait prima, tot de garbage collector steeds vaker langskomt en je P99-latency verdubbelt.
Claude Code herkent de klassiekers:
// string concatenatie in een loop — elke + maakt een nieuw string object
var result = "";
foreach (var item in items)
{
result += item.Name + ", ";
}
De fix is altijd StringBuilder, en Claude legt uit waarom: elke += alloceert een nieuw string object, kopieert alles, en laat het oude object over voor de garbage collector.
var sb = new StringBuilder();
foreach (var item in items)
{
sb.Append(item.Name);
sb.Append(", ");
}
var result = sb.ToString();
Andere allocatieproblemen die Claude oppikt: onnodige boxing van value types, LINQ-queries die in hot paths onnodig alloceren, en grote objecten die op de Large Object Heap terechtkomen en Gen 2 collections triggeren.
Hoe je prompts schrijft voor performance reviews
De kwaliteit van Claude’s analyse hangt af van hoe je vraagt. Dit zijn prompts die ik regelmatig gebruik:
Analyseer deze service op performanceproblemen. Let specifiek op:
- N+1 queries en onnodige database calls
- LINQ die in-memory filtert in plaats van in SQL
- Blokkerende async calls
- Onnodige allocaties in loops
Voor een breder overzicht:
Review deze ASP.NET Core controller op performance. De endpoints
worden gemiddeld 500 keer per minuut aangeroepen. Welke bottlenecks
zie je?
Dat getal — 500 keer per minuut — maakt verschil. Claude weegt zijn adviezen anders als het weet dat code onder load draait. Een kleine inefficiëntie in een endpoint dat eens per dag draait is acceptabel. Dezelfde inefficiëntie bij 500 requests per minuut is een probleem.
Eerlijke beperkingen
Claude Code is geen profiler. Het is belangrijk om dat scherp te houden.
Geen runtime data. Claude kan niet meten hoe lang een query daadwerkelijk duurt. Het herkent patronen die typisch traag zijn, maar of je specifieke query een probleem is hangt af van je data, je indexen, je hardware. Dat kan Claude niet weten.
Geen benchmarks. Claude kan je vertellen dat StringBuilder sneller is dan string concatenatie in een loop. Het kan je niet vertellen of het verschil in jouw case meetbaar is of verwaarloosbaar.
Heuristisch, niet empirisch. Claude’s analyse is gebaseerd op bekende anti-patronen en best practices. Het is een ervaren reviewer die zegt “dit ziet er verdacht uit” — niet een meetinstrument dat zegt “dit kost 340ms.”
Database-context ontbreekt. Claude weet niet of er een index zit op die kolom waar je op filtert. Het weet niet hoe groot je tabellen zijn. Het kan een full table scan niet voorspellen zonder die informatie.
Gebruik Claude Code als eerste pass. Valideer de bevindingen met een echte profiler — dotnet-trace, Application Insights, of gewoon een stopwatch in je code.
Begin hier
De volgende keer dat je een traag endpoint hebt, probeer dit voordat je een profiler opstart:
Dit endpoint is traag. Analyseer de code en identificeer mogelijke
performanceproblemen. Geef per probleem aan wat de impact is en
hoe ik het kan oplossen.
Je zult versteld staan hoe vaak het antwoord iets is dat je al wist maar niet zag — een N+1 query, een .ToList() op de verkeerde plek, een .Result die zich verstopt in een helper method.
Claude Code vervangt je profiler niet. Maar het vindt in tien seconden wat je anders een uur kost om te spotten. En dat maakt het een waardevolle eerste stap in elke performance-sessie.