100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Performance Optimization and Virtualization

How Blazor's diffing pipeline determines render cost, and the practical techniques — Virtualize, ShouldRender, and parameter hygiene — used to keep large apps fast.

Advanced BlazorAdvanced10 min readJul 10, 2026
Analogies

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.

razor
@* 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

Was this page helpful?

Topics covered

#BlazorStudyNotes#MicrosoftTechnologies#PerformanceOptimizationAndVirtualization#Performance#Optimization#Virtualization#Virtualize#StudyNotes#SkillVeris#ExamPrep