Performance Optimization and Virtualization
Blazor's rendering pipeline re-executes a component's BuildRenderTree logic whenever StateHasChanged is called (implicitly after event handlers or explicitly), then diffs the resulting RenderTreeFrame sequence against the previous render to compute the minimal DOM patch; the cost of this diff scales with the number of frames in the subtree being re-rendered, so the single biggest performance lever is controlling how much of the tree re-renders per update, not just how fast individual C# code executes. Common causes of unnecessary re-rendering include parent components re-rendering large child trees on every parent state change (fixed by overriding ShouldRender or splitting state into smaller child components), and passing new object/delegate instances as parameters on every render, which defeats Blazor's parameter-change short-circuiting for that child.
Cricket analogy: It's like a scoreboard operator repainting the entire stadium display for every run scored instead of updating just the runs-and-overs panel, wasting effort on unchanged sections like player photos and sponsor logos; efficient scoreboards, like efficient Blazor components, redraw only what actually changed.
Virtualize for Large Lists
The built-in Virtualize component renders only the subset of items currently visible (plus a small overscan buffer) within a scrollable container, instead of materializing every RenderTreeFrame for potentially thousands of list items up front; it measures the container and item size to calculate which item range is in view, and re-renders only that window as the user scrolls, which keeps both the initial render cost and per-scroll-frame cost roughly constant regardless of total list size. For genuinely large or server-backed datasets, Virtualize supports an ItemsProvider delegate that fetches only the requested page of data on demand (rather than requiring the full collection in memory client-side), which is essential when the underlying data source is a paged API or a large database table.
Cricket analogy: It's like a stadium video board showing only the current batting pair's stats in real time instead of trying to display all 22 players' career statistics simultaneously, keeping the display fast and legible; Virtualize similarly renders only the visible slice of a long list.
Reducing Renders with ShouldRender and Fine-Grained Parameters
Overriding ShouldRender() to return false when a component's own state hasn't meaningfully changed prevents wasted diff computation even if StateHasChanged was called on an ancestor; this is especially valuable for components with expensive markup (large tables, charts) nested inside frequently-updating parents. Equally important is avoiding parameter 'shape churn': passing a freshly-allocated lambda, anonymous object, or list instance as a component parameter on every parent render forces Blazor to treat the parameter as changed (since it can't structurally compare closures), triggering a child re-render even when the logical value is identical — caching delegates in fields, using EventCallback correctly, and passing primitive or record-equality-comparable values instead of fresh reference types avoids this.
Cricket analogy: It's like a team analyst refusing to re-run a full statistical model for a player whose numbers haven't changed since yesterday's data pull, even though the overall dashboard refreshed, saving compute; ShouldRender applies the same 'skip if unchanged' logic at the component level.
@* Virtualize with an ItemsProvider for a server-paged dataset *@
<Virtualize ItemsProvider="LoadOrdersAsync" Context="order" ItemSize="48">
<ItemContent>
<OrderRow Order="order" OnSelect="HandleSelect" />
</ItemContent>
<Placeholder>
<div class="skeleton-row"></div>
</Placeholder>
</Virtualize>
@code {
// Cache the delegate once instead of allocating a new lambda per render
private EventCallback<Order> HandleSelect;
protected override void OnInitialized()
{
HandleSelect = EventCallback.Factory.Create<Order>(this, SelectOrder);
}
private async ValueTask<ItemsProviderResult<Order>> LoadOrdersAsync(ItemsProviderRequest request)
{
var page = await OrderService.GetPageAsync(request.StartIndex, request.Count, request.CancellationToken);
return new ItemsProviderResult<Order>(page.Items, page.TotalCount);
}
private void SelectOrder(Order order) { /* ... */ }
}
@* Skip re-render when this component's own state is unchanged *@
@code {
private int _lastRenderedTotal = -1;
protected override bool ShouldRender()
{
if (Total == _lastRenderedTotal) return false;
_lastRenderedTotal = Total;
return true;
}
}
Wrapping every list item in Virtualize with @key using a stable, unique identifier (not the loop index) is critical — without it, Blazor may match up the wrong DOM elements during diffing as items scroll in and out, causing visible flicker, lost focus state, or animation glitches.
- Blazor's render cost scales with the size of the subtree being diffed, so limiting how much re-renders per update is the primary performance lever.
- The Virtualize component renders only visible items plus a small overscan buffer, keeping render cost roughly constant regardless of total list size.
- Virtualize's ItemsProvider delegate enables on-demand paged fetching for large or server-backed datasets instead of loading the entire collection client-side.
- Overriding ShouldRender() to skip re-rendering when a component's own state hasn't changed avoids wasted diff work even when an ancestor re-renders.
- Passing freshly-allocated lambdas or objects as parameters on every render defeats Blazor's parameter change-detection and forces unnecessary child re-renders.
- Splitting large components into smaller, independently re-rendering child components limits the blast radius of any single state change.
- Always use @key with a stable unique identifier inside virtualized or looped content to avoid incorrect DOM element reuse during diffing.
Practice what you learned
1. What primarily determines the cost of a Blazor re-render?
2. What does the Virtualize component's ItemsProvider enable?
3. Why can overriding ShouldRender() improve performance?
4. Why does passing a new lambda instance as a parameter on every parent render hurt performance?
5. Why is @key important when rendering a virtualized or looped list?
Was this page helpful?
You May Also Like
Render Modes in .NET 8+ (Interactive Server/WASM/Auto)
How .NET 8 unified Blazor hosting models into per-component render modes — Static, Interactive Server, Interactive WebAssembly, and Interactive Auto — and how to choose between them.
How Blazor Server Uses SignalR
A deep dive into the persistent SignalR circuit that powers Blazor Server, covering how events and DOM diffs flow, reconnection behavior, and bandwidth tuning.
Component Libraries: MudBlazor and Radzen
A practical comparison of MudBlazor and Radzen Blazor Components — pure-Razor UI libraries offering theming, forms, and data grids that work consistently across all render modes.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics