blazor-componentes-virtualizados-virtualize

Virtualized Components in Blazor with Virtualize

  • 5 min

A virtualized component is a list that only renders the visible elements, instead of painting all the data at once in the browser.

This is very useful when we have long lists, large tables, or paginated results. Because having 20 rows is one thing, but dumping 50,000 elements into the DOM and expecting the browser to smile politely is quite another.

Blazor includes the Virtualize component, which calculates which elements are visible on screen and renders only that portion. The remaining elements exist in the data, but they are not converted into HTML until needed.

The Problem with Large Lists

The most straightforward way to display a list in Blazor is to use a foreach.

@foreach (var product in products)
{
    <div class="product">
        <h3>@product.Name</h3>
        <p>@product.Price</p>
    </div>
}
Copied!

For a small list, this is perfectly fine. Simple, clear, and easy to read.

The problem arises when products has thousands of elements. Blazor has to create all the HTML nodes, the browser has to position them, calculate styles, measure positions, manage events… quite a party.

And on top of that, the user only sees 10 or 20 elements at a time.

Using Virtualize

To virtualize a list, we replace the foreach with the Virtualize component.

<Virtualize Items="@products" Context="product">
    <div class="product">
        <h3>@product.Name</h3>
        <p>@product.Price</p>
    </div>
</Virtualize>
Copied!

The idea is the same, but now Blazor only renders the elements necessary to cover the visible area.

The Items parameter receives the entire collection, and Context gives us the name of the variable we will use inside the template.

Virtualize doesn’t make loading data free. It makes rendering the interface much cheaper when we have many elements.

Container Height

Virtualization needs to identify a scrollable container. We can give it a limited height and enable vertical scrolling:

<div style="height: 500px; overflow-y: auto;">
    <Virtualize Items="@products" Context="product">
        <div class="product">
            @product.Name
        </div>
    </Virtualize>
</div>
Copied!

In a real application, we would move that style to a CSS class:

.product-list {
    height: 500px;
    overflow-y: auto;
}
Copied!

Item Size

Virtualize needs to estimate how much space each row occupies to calculate which elements to render. The rows must have the same height; if their size varies based on content, the scroll calculation may result in gaps or jumps.

By default, it can measure them, but if we know the approximate height, we can specify it with ItemSize. This helps the initial render be more stable and accurate.

<Virtualize Items="@products" Context="product" ItemSize="72">
    <ProductCard Product="product" />
</Virtualize>
Copied!

The value is in pixels. The closer it is to the actual size of each element, the more stable the scrolling will be. We can also adjust OverscanCount, which controls how many additional rows are rendered before and after the visible area.

Loading Data on Demand

So far, we have passed a complete list using Items. But often the data comes from an API or a database, and we don’t want to load 100,000 records just to show 20.

For that, we use ItemsProvider.

<Virtualize ItemsProvider="LoadProducts" Context="product">
    <ProductCard Product="product" />
</Virtualize>
Copied!

And in the component’s code:

private async ValueTask<ItemsProviderResult<Product>> LoadProducts(
    ItemsProviderRequest request)
{
    var result = await ProductService.GetPageAsync(
        startIndex: request.StartIndex,
        count: request.Count,
        cancellationToken: request.CancellationToken);

    return new ItemsProviderResult<Product>(
        result.Items,
        result.TotalCount);
}
Copied!

Blazor tells us from which index it needs data (StartIndex) and how many elements it wants (Count). Our service responds with that block and the total count of available items. We also propagate the CancellationToken: when scrolling quickly, a previous request may no longer be necessary.

This starts to look like pagination, but with a much smoother experience for the user.

Placeholder and Empty List

When loading data from an API, there may be a slight delay. To avoid leaving the interface “dead”, we can use Placeholder.

<Virtualize ItemsProvider="LoadProducts" Context="product">
    <ItemContent>
        <ProductCard Product="product" />
    </ItemContent>
    <Placeholder>
        <div class="product skeleton">Loading...</div>
    </Placeholder>
    <EmptyContent>
        <p>No products to display.</p>
    </EmptyContent>
</Virtualize>
Copied!

Placeholder is displayed while the elements are loading. EmptyContent is displayed when there are no results.

These are small details, but they greatly impact the perceived quality of an interface.

If the filters or sorting change, we can keep a reference to the component and request the data again:

<Virtualize @ref="list" ItemsProvider="LoadProducts" Context="product">
    <ProductCard Product="product" />
</Virtualize>

@code {
    private Virtualize<Product>? list;

    private async Task ApplyFiltersAsync()
    {
        await list!.RefreshDataAsync();
    }
}
Copied!

The event handler already triggers a new render. If we invoke RefreshDataAsync from a background task, we must also call StateHasChanged.

When to Use Virtualize

Virtualize is very useful in these cases:

  • Long result lists.
  • Tables with hundreds or thousands of rows.
  • Feeds, logs, or histories.
  • Components that repeat relatively heavy cards.

It’s not worth using for a list of 10 elements. A regular foreach is clearer and sufficient there.

Use Virtualize when the problem is rendering many elements. If the database query is too slow, review the query, indexes, pagination, or cache.