Rendering thousands of records in a Blazor application can quickly become expensive. A normal foreach loop renders every item into the DOM, even when the user can see only a small portion of the list.
Blazor provides the Virtualize<TItem> component to solve this problem. Instead of rendering the complete collection, Virtualize renders the items needed for the visible area and uses spacer elements to represent content that isn't currently rendered.
Virtualization works particularly well for large tables, activity feeds, product lists, employee directories, log viewers, and other data-heavy screens.
There has traditionally been an important limitation, though: virtualization works best when list items have a predictable height. If one item becomes taller because it contains additional text, an expanded section, a loaded image, or dynamically generated content, the estimated positions of items below it can change.
That can lead to a familiar problem: the user scrolls through a list and the visible content suddenly moves.
.NET 11 improves Virtualize<TItem> so it can better handle items whose heights change at runtime. The component now uses measured item sizes and browser scroll anchoring where supported, with additional compensation for scenarios where native anchoring isn't sufficient.
This makes variable-height virtualized lists much more practical, but it doesn't mean every virtualization layout is automatically correct. The component still needs a suitable layout, sensible initial sizing, and careful handling of dynamic content.
What Is Virtualization in Blazor?
Suppose an application needs to display 50,000 records.
Without virtualization:
@foreach (var product in Products)
{
<ProductCard Product="product" />
}
Blazor creates DOM elements for all 50,000 records.
Even if the browser viewport displays only 20 products, all 50,000 elements can still exist in the rendered page.
With Virtualize:
<Virtualize Items="Products" Context="product">
<ProductCard Product="product" />
</Virtualize>
Blazor renders only the portion of the list that needs to be visible, plus additional items around the visible region.
Conceptually, the structure becomes:
Top spacer
Item
Item
Item
Item
Visible items
Item
Item
Item
Bottom spacer
The spacer elements represent the space occupied by items that aren't currently rendered.
As the user scrolls, Virtualize changes which records are rendered.
This reduces the number of DOM elements and the amount of component rendering required for large collections.
Why Variable-Height Items Are Difficult
Virtualization needs to answer a basic question:
Which item belongs at this scroll position?
If every item is approximately 60 pixels high, the calculation is straightforward.
For example:
Item 0 60 px
Item 1 60 px
Item 2 60 px
Item 3 60 px
...
If the user scrolls 600 pixels, the component can estimate that the viewport is around item 10.
The calculation becomes harder when item heights vary:
Item 0 60 px
Item 1 120 px
Item 2 60 px
Item 3 220 px
Item 4 60 px
Now the same scroll position doesn't correspond to a predictable item index.
The problem becomes even more noticeable when an item changes height after it has already been rendered.
For example:
Initial:
Item 10 70 px
Item 11 70 px
Item 12 70 px
After content loads:
Item 10 70 px
Item 11 180 px
Item 12 70 px
Everything below item 11 moves.
If the virtualization system doesn't compensate for that change, the user's viewport can appear to jump.
What Changed in .NET 11?
.NET 11 improves Virtualize<TItem> specifically for variable-height content.
Earlier implementations relied heavily on the assumption that virtualized items had a consistent height. Applications could experience visible content movement when an item above the viewport changed size.
.NET 11 adapts to measured item sizes at runtime. It also makes better use of browser scroll anchoring where supported and provides a compensation mechanism for scenarios such as table layouts and browsers where native anchoring isn't sufficient.
This is an important improvement because dynamic content is common in real applications.
For example:
A message expands when the user clicks "Show more."
An image finishes loading.
API data changes the content of a card.
Validation messages appear.
A component expands or collapses.
A previously hidden section becomes visible.
The important point is that developers generally don't need to implement a separate scroll-position algorithm for these normal virtualization scenarios.
Basic Virtualize Example
A simple virtualized list looks like this:
<div class="list-container">
<Virtualize Items="Products" Context="product">
<div class="product-row">
<strong>@product.Name</strong>
<span>@product.Category</span>
</div>
</Virtualize>
</div>
@code {
private List<Product> Products = [];
protected override void OnInitialized()
{
Products = Enumerable.Range(1, 10000)
.Select(i => new Product
{
Id = i,
Name = $"Product {i}",
Category = i % 2 == 0 ? "Hardware" : "Software"
})
.ToList();
}
private sealed class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Category { get; set; } = "";
}
}
The scroll container needs a defined height:
.list-container {
height: 600px;
overflow-y: auto;
}
.product-row {
padding: 12px;
border-bottom: 1px solid #ddd;
}
For a fixed-height list, this is straightforward.
The more interesting case is when product-row can change height.
Building a Variable-Height Item
Consider a product description that can expand.
<Virtualize Items="Products" Context="product">
<div class="product-card">
<h3>@product.Name</h3>
<p>@product.Description</p>
@if (product.ShowDetails)
{
<div class="details">
<p>SKU: @product.Sku</p>
<p>Category: @product.Category</p>
<p>Additional information about this product.</p>
</div>
}
<button @onclick="() => ToggleDetails(product)">
@(product.ShowDetails ? "Hide Details" : "Show Details")
</button>
</div>
</Virtualize>
@code {
private void ToggleDetails(Product product)
{
product.ShowDetails = !product.ShowDetails;
}
}
The height of the card changes when ShowDetails changes.
In .NET 11, Virtualize can adapt to these runtime size changes more effectively than previous implementations.
The important part is that the component should still be allowed to maintain its expected vertical layout.
Why ItemSize Still Matters
The improved variable-height behavior does not make ItemSize irrelevant.
ItemSize provides an initial estimate for the height of an item.
For example:
<Virtualize Items="Products"
ItemSize="80"
Context="product">
<ProductCard Product="product" />
</Virtualize>
If an item is normally around 80 pixels tall, specifying that value gives the virtualization system a useful starting point.
The default value is 50 pixels.
An accurate initial estimate is particularly useful when the user loads or refreshes a page at a particular scroll position. If the initial calculation is significantly wrong, the browser may initially display the wrong region before actual item measurements become available.
For variable-height content, choose an estimate that represents the typical rendered item rather than the smallest or largest possible item.
For example, if your cards usually range from 70 to 130 pixels, an initial value around the typical size is more useful than choosing 70 simply because it is the minimum.
Measuring Actual Item Heights
Virtualize measures rendered items and maintains information about their actual sizes.
Conceptually, the process looks like this:
Initial render
|
v
Use ItemSize estimate
|
v
Render visible items
|
v
Measure actual item sizes
|
v
Update virtualization calculations
|
v
Continue scrolling
This is why setting a reasonable ItemSize still matters even when the list contains variable-height items.
The initial estimate gets the component close to the correct position. Runtime measurements then improve the positioning as more items enter the viewport.
OverscanCount and Scroll Performance
OverscanCount controls how many additional items are rendered before and after the visible area.
For example:
<Virtualize Items="Products"
ItemSize="80"
OverscanCount="20"
Context="product">
<ProductCard Product="product" />
</Virtualize>
A higher overscan value means more items remain rendered around the viewport.
This can make scrolling feel smoother because the component has more content ready before the user reaches it.
However, a larger overscan value also means more components exist in the DOM.
The general trade-off is:
Overscan | Advantage | Trade-off |
|---|---|---|
Low | Fewer rendered elements | More frequent virtualization updates |
Medium | Balanced behavior | Moderate DOM size |
High | More content ready during scrolling | More rendered elements |
Very high | Less frequent item replacement | Can reduce the benefits of virtualization |
.NET 11 increases the default OverscanCount from 3 to 15. This also provides more measured items for calculating average item height.
You should still tune OverscanCount based on the actual component complexity rather than assuming a higher value is always better.
Using ItemsProvider for Large Data Sets
Virtualization becomes more useful when the complete dataset doesn't need to be loaded into memory.
Instead of:
<Virtualize Items="AllProducts">
you can use:
<Virtualize ItemsProvider="LoadProducts"
Context="product">
<div class="product-row">
<strong>@product.Name</strong>
<span>@product.Category</span>
</div>
</Virtualize>
The provider receives information about the requested range.
For example:
private async ValueTask<ItemsProviderResult<Product>> LoadProducts(
ItemsProviderRequest request)
{
var products = await ProductService.GetProductsAsync(
request.StartIndex,
request.Count);
var totalCount = await ProductService.GetProductCountAsync();
return new ItemsProviderResult<Product>(
products,
totalCount);
}
This approach is useful when the data comes from a database or API.
Instead of loading 100,000 records into the browser, the application can request the portion needed by the virtualized viewport.
Combining ItemsProvider with Variable Heights
A variable-height list backed by an API might look like this:
<div class="product-list">
<Virtualize ItemsProvider="LoadProducts"
ItemSize="100"
OverscanCount="15"
Context="product">
<div class="product-card">
<h3>@product.Name</h3>
<p>@product.Description</p>
@if (product.HasAdditionalInformation)
{
<div class="additional-info">
@product.AdditionalInformation
</div>
}
</div>
</Virtualize>
</div>
Join the conversation! Your thoughts help the community grow.