Performance & Virtualization
Key Points
<Virtualize>for large lists (1000+ items). Renders only visible rows. Massive perf win.ShouldRender+@key+ immutable parameters are the three big tools to avoid wasted re-renders.- WASM payload optimization: trimming, AOT, compression, lazy loading.
- Interactive Server: minimize chatty events; debounce input; backplane for scale.
- Diagnostics:
dotnet-counters, browser perf tools, Blazor render-tree visualization (Browser DevTools).
Concepts (deep dive)
<Virtualize>
<Virtualize Items="@_orders" Context="order" ItemSize="50">
<ItemContent>
<OrderRow Order="@order" />
</ItemContent>
<Placeholder>
<p>Loading...</p>
</Placeholder>
</Virtualize>
Renders only visible rows. Reuses DOM as you scroll. 100,000 rows feels instant.
ItemSize: pixel height; helps initial sizing. Default 50.
<Virtualize> with async data
<Virtualize ItemsProvider="@LoadItems" Context="item">
<ItemContent>...</ItemContent>
</Virtualize>
@code {
async ValueTask<ItemsProviderResult<Order>> LoadItems(ItemsProviderRequest req)
{
var items = await Service.GetOrdersPaged(req.StartIndex, req.Count);
return new ItemsProviderResult<Order>(items, totalCount: 100_000);
}
}
Server fetches only the visible window.
@key
Without @key, Blazor's diff matches by position — reorders create unnecessary work. With @key, identity is tracked properly.
ShouldRender
private int _lastRenderedId;
protected override bool ShouldRender()
{
if (Item.Id == _lastRenderedId) return false;
_lastRenderedId = Item.Id;
return true;
}
Skip the diff entirely when nothing relevant changed.
Immutable parameter values
When parent passes a record/value type, Blazor can fast-compare. Prefer:
Over:
Avoid heavy computation in render
<!-- ❌ runs every render -->
<p>@(_items.Where(i => i.Active).Sum(i => i.Total))</p>
<!-- ✅ memoize -->
<p>@_total</p>
@code { decimal _total; protected override void OnParametersSet() => _total = ...; }
Debounce input
<input @oninput="OnInput" />
@code {
System.Timers.Timer? _timer;
string _query = "";
void OnInput(ChangeEventArgs e)
{
_query = e.Value?.ToString() ?? "";
_timer?.Dispose();
_timer = new(300);
_timer.Elapsed += async (_, _) => { await InvokeAsync(Search); _timer?.Dispose(); };
_timer.Start();
}
Task Search() { /* fetch */; StateHasChanged(); return Task.CompletedTask; }
}
Server-side: minimize SignalR traffic
Per-keystroke server callbacks → flood. Debounce. Or use oninput only when needed.
WASM bundle optimization
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<RunAOTCompilation>true</RunAOTCompilation> <!-- bigger but faster runtime -->
<BlazorWebAssemblyJiterpreter>true</BlazorWebAssemblyJiterpreter>
<BlazorEnableCompression>true</BlazorEnableCompression>
</PropertyGroup>
Trade-offs: - Trimming: smaller (-30-50%); but reflection-heavy code may break. - AOT: 2-5x faster runtime; bundle 2x larger. - Compression: brotli auto-applied; CDN must serve it.
Lazy-load assemblies
Don't ship rarely-used features in initial download.
Component code splitting
For large components, split into smaller — reduces re-render scope.
Render frequency profiling
Browser DevTools → Performance → Record. See render times per component.
Or:
[Render mode]
override Task OnAfterRenderAsync(bool firstRender)
{
Console.WriteLine($"Rendered at {DateTime.Now}");
}
Diagnose unnecessary renders
Add a render counter to a component:
private int _renderCount;
protected override void OnAfterRender(bool firstRender) { _renderCount++; }
Display it; click around; observe.
EventCallback<T> over Action
Action triggers parent re-render only if you call StateHasChanged. EventCallback auto-StateHasChanged. But also avoids unnecessary capture.
Hot reload
Dev: dotnet watch run. Edit Razor; instant in browser. Doesn't measure perf — that's a Release-build concern.
Static SSR perf
Static SSR is fastest (no client runtime). Use for content-heavy pages.
Interactive Server scale
- Backplane (Azure SignalR / Redis).
- Server resource: each circuit ~50-100KB memory.
- 1000s concurrent fine; 100K needs Azure SignalR.
Memory leaks
- Forgot Dispose on event subscriptions.
- DotNetObjectReference not disposed.
- Captured closures in long-lived contexts.
Compression
Server: enable response compression for static assets. WASM: brotli + gzip.
Code: correct vs wrong
❌ Wrong: render 10K rows directly
Browser tab freezes.
✅ Correct: Virtualize
<Virtualize Items="@_orders" Context="o" ItemSize="50">
<ItemContent><OrderRow Order="@o" /></ItemContent>
</Virtualize>
❌ Wrong: missing @key
Reorders inefficient.
✅ Correct: @key
Design patterns for this topic
Pattern 1 — "Virtualize for large lists"
- Intent: O(visible) instead of O(total).
Pattern 2 — "@key for identity"
- Intent: correct diff on reorder.
Pattern 3 — "ShouldRender to skip"
- Intent: avoid diff cost.
Pattern 4 — "Debounce input"
- Intent: reduce traffic / re-render spam.
Pattern 5 — "Trim + compress WASM"
- Intent: smaller download.
Pros & cons / trade-offs
| Optimization | Pros | Cons |
|---|---|---|
| Virtualize | Massive list perf | Setup; not all layouts |
| Trim | Smaller WASM | Reflection breakage |
| AOT | Fast runtime | Bigger bundle |
| Lazy load | Smaller initial | Setup |
When to use / when to avoid
- Always Virtualize for >100 items.
- Always @key in lists.
- Use trim + compress in production.
- Avoid AOT unless runtime perf critical.
Interview Q&A
Q1. Virtualize purpose? Render only visible rows. O(visible) cost. Critical for large lists.
Q2. ItemsProvider? Async data source for Virtualize. Server fetches windowed.
Q3. @key? Identity tracking for diff. Without it, reorders inefficient.
Q4. ShouldRender? Skip diff entirely when nothing relevant changed.
Q5. EventCallback vs Action? EventCallback auto-StateHasChanged parent. Action doesn't.
Q6. Trimming risks? Reflection-heavy code may break. Test thoroughly.
Q7. AOT? Compiles WASM ahead-of-time → faster runtime; larger bundle.
Q8. Lazy load assemblies? Defer rarely-used DLLs. <BlazorWebAssemblyLazyLoad>.
Q9. Debounce in Blazor? Timer-based or Task.Delay with cancellation. Reduces re-renders.
Q10. Interactive Server scale? Backplane (Azure SignalR / Redis); per-circuit memory budget.
Q11. Profiling Blazor? Browser DevTools Performance + render counters.
Q12. Memory leaks? Undisposed JS refs, event subs, closures.
Gotchas / common mistakes
- ⚠️ Long lists without Virtualize — frozen tabs.
- ⚠️ No @key — wasteful re-renders.
- ⚠️ Heavy LINQ in Razor — recomputed every render.
- ⚠️ No debounce on inputs — flood.
- ⚠️ No compression — fat WASM downloads.