Introduction
Component virtualization is a technique for limiting UI rendering to only the visible parts of a long list. This dramatically improves performance when rendering large data sets.
Without virtualization, rendering 10,000 items would create 10,000 DOM elements. With virtualization, only the visible items (typically 10-20) are rendered, plus a small buffer.
When to Use Virtualization
| Scenario Recommendation |
| < 100 items | Regular foreach is fine |
| 100 - 1,000 items | Consider virtualization |
| > 1,000 items | Strongly recommended |
The Virtualize Component
The Virtualize<TItem> component is a built-in Blazor component that efficiently renders lists by only creating DOM elements for visible items.
Important: The Virtualize component requires an interactive render mode to function. It will not work with static server-side rendering (SSR).
Render Mode Requirement
The Virtualize component needs interactivity to handle scroll events. Add one of the following render modes to your page:
@page "/users"
@rendermode InteractiveServer
Or for WebAssembly:
@page "/users"
@rendermode InteractiveWebAssembly
| Render Mode Description |
InteractiveServer | Server-side interactivity via SignalR |
InteractiveWebAssembly | Client-side interactivity via WebAssembly |
InteractiveAuto | Starts with Server, then switches to WebAssembly |
Basic Usage
Using Items Property
The simplest way to use virtualization with an in-memory collection:
@page "/users"
@rendermode InteractiveServer
<div style="height: 400px; overflow-y: auto;">
<Virtualize Items="users" Context="user">
<div class="user-item">
<strong>@user.Name</strong> - @user.Email
</div>
</Virtualize>
</div>
@code {
private List<User> users = [];
protected override void OnInitialized()
{
users = Enumerable.Range(1, 10000)
.Select(i => new User { Name = $"User {i}", Email = $"user{i}@example.com" })
.ToList();
}
private record User
{
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
}
}
Important: The container must have a fixed height and overflow-y: auto for virtualization to work.
ItemsProvider for Async Loading
For large datasets, use ItemsProvider to load items on-demand:
<Virtualize ItemsProvider="LoadItemsAsync" Context="item">
<div>@item.Name</div>
</Virtualize>
@code {
private async ValueTask<ItemsProviderResult<Item>> LoadItemsAsync(
ItemsProviderRequest request)
{
// request.StartIndex - First item index to load
// request.Count - Number of items to load
// request.CancellationToken - Cancellation token
var items = await DataService.GetItemsAsync(
request.StartIndex,
request.Count,
request.CancellationToken);
var totalCount = await DataService.GetTotalCountAsync();
return new ItemsProviderResult<Item>(items, totalCount);
}
}
ItemsProviderRequest Properties
| PropertyTypeDescription |
StartIndex | int | Index of the first item to load |
Count | int | Number of items requested |
CancellationToken | CancellationToken | Cancellation token for the request |
ItemsProviderResult Constructor
new ItemsProviderResult<T>(IEnumerable<T> items, int totalItemCount)
Placeholder Template
Show a loading indicator while items are being fetched:
<Virtualize ItemsProvider="LoadItemsAsync" Context="item">
<ItemContent>
<div class="item">
<strong>@item.Name</strong>
<span>@item.Description</span>
</div>
</ItemContent>
<Placeholder>
<div class="item placeholder-glow">
<span class="placeholder col-6"></span>
<span class="placeholder col-4"></span>
</div>
</Placeholder>
</Virtualize>
Empty Content Template
Display a message when the collection is empty:
<Virtualize Items="items" Context="item">
<ItemContent>
<div>@item.Name</div>
</ItemContent>
<EmptyContent>
<div class="alert alert-info">
No items found. Try adjusting your search criteria.
</div>
</EmptyContent>
</Virtualize>
Configuring Item Size
The ItemSize parameter specifies the approximate height (in pixels) of each item:
<Virtualize Items="items" Context="item" ItemSize="80">
<div style="height: 80px;">
@item.Name
</div>
</Virtualize>
| PropertyDefaultDescription |
ItemSize | 50 | Height of each item in pixels |
Tip: Set ItemSize to match your actual item height for smoother scrolling.
Configuring Overscan
The OverscanCount parameter specifies how many extra items to render above and below the visible area:
<Virtualize Items="items" Context="item" OverscanCount="10">
<div>@item.Name</div>
</Virtualize>
| Property Default Description |
OverscanCount | 3 | Number of extra items to render outside the viewport |
Higher values:
- ✅ Smoother scrolling experience
- ❌ More memory usage
Lower values:
- ✅ Less memory usage
- ❌ May see brief flashes during fast scrolling
SpacerElement Customization
Customize the spacer element used for scroll calculations:
<Virtualize Items="items" Context="item" SpacerElement="tr">
<tr>
<td>@item.Name</td>
<td>@item.Value</td>
</tr>
</Virtualize>
Useful for HTML tables where spacers need to be <tr> elements.
Refreshing Data
Call RefreshDataAsync() to reload the data:
<Virtualize @ref="virtualizeComponent" Items="items" Context="item">
<div>@item.Name</div>
</Virtualize>
<button @onclick="RefreshAsync">Refresh</button>
@code {
private Virtualize<Item>? virtualizeComponent;
private List<Item> items = [];
private async Task RefreshAsync()
{
items = await LoadNewDataAsync();
if (virtualizeComponent is not null)
{
await virtualizeComponent.RefreshDataAsync();
}
}
}
Complete Example with All Features
@page "/products"
@rendermode InteractiveServer
@inject IProductService ProductService
<PageTitle>Products</PageTitle>
<div class="search-box mb-3">
<input @bind="searchTerm" @bind:event="oninput"
placeholder="Search products..."
class="form-control" />
</div>
<div style="height: 500px; overflow-y: auto;">
<Virtualize @ref="virtualize"
ItemsProvider="LoadProductsAsync"
Context="product"
ItemSize="70"
OverscanCount="5">
<ItemContent>
<div class="product-card d-flex justify-content-between p-3 border-bottom">
<div>
<h5>@product.Name</h5>
<small class="text-muted">@product.Category</small>
</div>
<div class="text-end">
<span class="badge bg-primary">$@product.Price</span>
</div>
</div>
</ItemContent>
<Placeholder>
<div class="product-card d-flex justify-content-between p-3 border-bottom">
<div class="placeholder-glow" style="width: 60%;">
<span class="placeholder col-8"></span>
<span class="placeholder col-4"></span>
</div>
<span class="placeholder col-2"></span>
</div>
</Placeholder>
<EmptyContent>
<div class="alert alert-warning text-center">
No products found matching "@searchTerm"
</div>
</EmptyContent>
</Virtualize>
</div>
@code {
private Virtualize<Product>? virtualize;
private string searchTerm = string.Empty;
private async ValueTask<ItemsProviderResult<Product>> LoadProductsAsync(
ItemsProviderRequest request)
{
var result = await ProductService.GetProductsAsync(
searchTerm,
request.StartIndex,
request.Count,
request.CancellationToken);
return new ItemsProviderResult<Product>(result.Items, result.TotalCount);
}
private async Task OnSearchChanged()
{
if (virtualize is not null)
{
await virtualize.RefreshDataAsync();
}
}
}
Performance Comparison
Without Virtualization (10,000 items)
@foreach (var item in items)
{
<div>@item.Name</div>
}
- ❌ Creates 10,000 DOM elements
- ❌ Slow initial render (seconds)
- ❌ High memory usage
- ❌ Sluggish scrolling
With Virtualization (10,000 items)
<Virtualize Items="items" Context="item">
<div>@item.Name</div>
</Virtualize>
- ✅ Creates ~20-30 DOM elements
- ✅ Fast initial render (milliseconds)
- ✅ Low memory usage
- ✅ Smooth scrolling
Common Issues and Solutions
| Issue Cause Solution |
| List is empty/not rendering | Missing render mode | Add @rendermode InteractiveServer or InteractiveWebAssembly |
| Items don't scroll | Container has no height | Add height and overflow-y: auto to container |
| Blank spaces appear | ItemSize doesn't match actual | Set ItemSize to match your item height |
| Flashing during scroll | OverscanCount too low | Increase OverscanCount |
| Items reload constantly | Missing CancellationToken | Use request.CancellationToken in async calls |
Summary
| Parameter Type Default Description |
Items | ICollection<TItem> | - | In-memory collection |
ItemsProvider | ItemsProviderDelegate<TItem> | - | Async data provider |
ItemContent | RenderFragment<TItem> | - | Template for each item |
Placeholder | RenderFragment<PlaceholderContext> | - | Loading template |
EmptyContent | RenderFragment | - | Empty state template |
ItemSize | float | 50 | Item height in pixels |
OverscanCount | int | 3 | Extra items to render |
SpacerElement | string | "div" | Spacer element type |