Skip to content

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

@foreach (var item in _items)
{
    <ItemRow @key="item.Id" Item="@item" />
}

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:

public record OrderViewModel(Guid Id, string Name);

Over:

public class OrderViewModel { /* mutable; reference equality only */ }

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

<BlazorWebAssemblyLazyLoad Include="Heavy.dll" />
await assemblies.LoadFilesAsync("Heavy.dll");

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

@foreach (var o in _orders) { <OrderRow Order="@o" /> }

Browser tab freezes.

✅ Correct: Virtualize

<Virtualize Items="@_orders" Context="o" ItemSize="50">
    <ItemContent><OrderRow Order="@o" /></ItemContent>
</Virtualize>

❌ Wrong: missing @key

@foreach (var item in _items) { <ItemRow Item="@item" /> }

Reorders inefficient.

✅ Correct: @key

@foreach (var item in _items) { <ItemRow @key="item.Id" Item="@item" /> }

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.

Further reading