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:

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>

The key design decision is to keep the initial ItemSize close to the expected normal card height.

The actual content can then be measured as it becomes visible.

Use @key for Stable Item Identity

When rendering dynamic lists, stable identity is important.

For example:

<Virtualize Items="Products" Context="product">
    <ProductCard @key="product.Id"
                 Product="product" />
</Virtualize>

The @key directive tells Blazor which rendered component corresponds to which data item.

This is particularly useful when items are inserted, removed, reordered, or updated.

Without stable identity, component instances can be reused in ways that aren't appropriate for stateful child components.

A good key should represent the actual identity of the record:

@key="product.Id"

rather than using a temporary list index when a stable database identifier is available.

Dynamic Images and Variable Heights

Images are a common reason list items change size.

This can happen when the image dimensions aren't known before the browser loads the image.

For example:

<div class="product-card">
    <img src="@product.ImageUrl"
         alt="@product.Name" />

    <h3>@product.Name</h3>

    <p>@product.Description</p>
</div>

If the image loads after the initial render and changes the card height, the list layout can change.

A better approach is to reserve the expected image space when possible:

.product-image {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
}

Then:

<img class="product-image"
     src="@product.ImageUrl"
     alt="@product.Name" />

This is good UI practice regardless of virtualization because it reduces layout shifts.

.NET 11's virtualization improvements help when the size actually changes, but predictable layouts are still preferable.

Expand and Collapse Content Carefully

Variable-height virtualization works particularly well for expandable content, but there is an important UX consideration.

Consider:

@if (expanded)
{
    <div class="details">
        ...
    </div>
}

When this section appears, the item's height changes.

If the expanded item is above the current viewport, its height change affects the position of everything below it.

The improved virtualization behavior helps maintain the user's scroll position.

However, developers should still avoid unnecessary state changes that repeatedly expand and collapse large numbers of items.

For example, a list where every item automatically expands after every render can cause unnecessary layout work.

Keep expansion state explicit:

private readonly HashSet<int> expandedProducts = [];

private void ToggleProduct(int productId)
{
    if (!expandedProducts.Add(productId))
    {
        expandedProducts.Remove(productId);
    }
}

Then:

@if (expandedProducts.Contains(product.Id))
{
    <div class="details">
        Additional product information.
    </div>
}

Tables Require Extra Care

Virtualizing table rows has stricter layout requirements.

A typical structure is:

<table>
    <thead>
        <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Status</th>
        </tr>
    </thead>

    <tbody>
        <Virtualize Items="Orders"
                    ItemSize="45"
                    Context="order"
                    SpacerElement="tr">

            <tr @key="order.Id">
                <td>@order.Id</td>
                <td>@order.Name</td>
                <td>@order.Status</td>
            </tr>

        </Virtualize>
    </tbody>
</table>

The structure and CSS must allow the virtualized rows and spacer elements to remain a single vertical stack.

Table layouts are also one of the scenarios where browser-native scroll anchoring isn't always sufficient, so .NET 11 includes additional compensation for table virtualization.

Don't apply arbitrary CSS to the generated spacer elements. Styling that changes their size or layout can interfere with virtualization calculations.

CSS Layout Rules Matter

Virtualize isn't a general-purpose layout engine.

For a typical div-based list, the scroll container can be:

.list-container {
    height: 600px;
    overflow-y: auto;
}

The immediate virtualized content should remain in a vertical stack.

For example:

.product-card {
    display: block;
    width: 100%;
}

If using a flex layout, make sure the children don't unexpectedly shrink.

A problematic layout can look like:

.list-container {
    display: flex;
    flex-direction: column;
}

.list-container > div {
    flex-shrink: 1;
}

When virtualization spacer elements or content items are affected by layout rules that change their expected dimensions, the component may not behave correctly.

Common Causes of Scroll Jumps

Even with .NET 11 improvements, investigate these areas if a list still jumps.

Incorrect ItemSize

If the initial estimate is far from the actual item size, the initial viewport calculation can be inaccurate.

Use a realistic value:

ItemSize="90"

rather than an arbitrary value.

Images Without Reserved Space

Images that change their dimensions after loading can cause layout shifts.

Use known dimensions or an aspect ratio where possible.

Unstable Item Identity

Use:

@key="item.Id"

when items have stable identifiers.

Excessive DOM Changes

Avoid repeatedly replacing large portions of the list when only one item changed.

CSS Affecting Spacer Elements

Don't override the styles or layout behavior of Virtualize's spacer elements.

Complex Nested Components

A virtualized item containing many nested components can still be expensive to render.

Virtualization reduces the number of rendered items, but it doesn't make each item free.

Common Mistakes

Mistake 1: Assuming Virtualize Makes Every List Fast

Virtualization reduces the number of rendered items.

It doesn't automatically optimize:

Mistake 2: Setting ItemSize to an Arbitrary Number

This:

ItemSize="20"

isn't automatically better than:

ItemSize="100"

Choose an estimate based on the actual UI.

Mistake 3: Increasing Overscan Excessively

A large value can reduce the frequency of list replacement, but it also increases the number of rendered elements.

Don't solve every scrolling issue by increasing OverscanCount.

Mistake 4: Ignoring CSS

A virtualization component cannot compensate for CSS that fundamentally breaks its expected vertical layout.

Mistake 5: Loading the Entire Database into Memory

This:

var products = await db.Products.ToListAsync();

followed by:

<Virtualize Items="products">

still loads the entire dataset.

For genuinely large datasets, use an ItemsProvider and server-side paging.

Troubleshooting Checklist

When a virtualized list doesn't behave correctly, check the following.

1. Confirm the Scroll Container

Make sure the container has a defined scrolling region:

.list-container {
    height: 600px;
    overflow-y: auto;
}

2. Check ItemSize

Ask whether the configured estimate resembles the normal item height.

ItemSize="80"

3. Check for Dynamic Content

Look for:

4. Check @key

Use stable identifiers:

@key="item.Id"

5. Check OverscanCount

Start with the default behavior before making aggressive changes.

If necessary:

OverscanCount="20"

but verify the effect on rendering and memory usage.

6. Inspect CSS

Make sure the virtualized content remains a proper vertical stack.

7. Test Deep Scrolling

Don't test only the first few items.

Scroll through the middle and lower portions of the dataset, where virtualization calculations are more noticeable.

Advantages of Blazor .NET 11 Virtualize

Better Variable-Height Support

The component can respond to measured item sizes instead of relying entirely on a fixed-height assumption.

Better Scroll Stability

Runtime size changes are handled more effectively, reducing visible jumps.

Less Manual JavaScript

Developers don't need to build a custom scroll-position compensation system for common variable-height scenarios.

Efficient Large Lists

Only a subset of records needs to be rendered at any one time.

Works with Data Providers

ItemsProvider allows virtualized lists to request data as needed rather than loading everything upfront.

Disadvantages and Trade-Offs

Virtualization Adds Layout Complexity

A normal foreach loop is simpler and can be preferable for small lists.

Variable Heights Still Require Thoughtful UI Design

.NET 11 improves the behavior, but predictable dimensions are still better for initial rendering.

Complex Items Can Remain Expensive

If each item contains a large component tree, rendering even a small visible subset can still be costly.

CSS Can Break Virtualization

Layouts that interfere with the expected vertical structure can produce incorrect behavior.

Data Loading Still Matters

Virtualization does not automatically make database queries efficient. The data layer still needs appropriate filtering, paging, indexing, and caching strategies.

Best Practices

  1. Use Virtualize when the list is large enough that rendering every item is unnecessary.

  2. Give ItemSize a realistic initial estimate.

  3. Let .NET 11 measure actual item sizes rather than implementing custom height calculations unnecessarily.

  4. Use stable @key values for stateful list items.

  5. Reserve space for images when their dimensions are known.

  6. Use ItemsProvider for datasets that shouldn't all be loaded into memory.

  7. Tune OverscanCount only after observing actual scrolling behavior.

  8. Keep virtualized item components reasonably lightweight.

  9. Avoid CSS that changes the layout or dimensions of virtualization spacer elements.

  10. Test variable-height changes above and inside the current viewport.

  11. Test table virtualization separately from normal div layouts.

  12. Don't assume virtualization alone solves database or API performance problems.

Production Example

A production-oriented product list might combine virtualization, server-side data loading, stable identity, a realistic initial size, and moderate overscan:

<div class="product-list">
    <Virtualize ItemsProvider="LoadProducts"
                ItemSize="110"
                OverscanCount="15"
                Context="product">

        <article class="product-card" @key="product.Id">
            <img class="product-image"
                 src="@product.ImageUrl"
                 alt="@product.Name" />

            <div class="product-content">
                <h3>@product.Name</h3>

                <p>@product.Description</p>

                @if (product.IsExpanded)
                {
                    <div class="product-details">
                        <p>SKU: @product.Sku</p>
                        <p>Category: @product.Category</p>
                    </div>
                }

                <button @onclick="() => ToggleDetails(product)">
                    @(product.IsExpanded ? "Show Less" : "Show More")
                </button>
            </div>
        </article>

    </Virtualize>
</div>

@code {
    private async ValueTask<ItemsProviderResult<Product>> LoadProducts(
        ItemsProviderRequest request)
    {
        var result = await ProductService.GetProductsAsync(
            request.StartIndex,
            request.Count);

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

    private void ToggleDetails(Product product)
    {
        product.IsExpanded = !product.IsExpanded;
    }
}

The associated layout can reserve image space:

.product-list {
    height: 650px;
    overflow-y: auto;
}

.product-card {
    display: flex;
    gap: 16px;
    width: 100%;
    padding: 12px;
    box-sizing: border-box;
}

.product-image {
    width: 120px;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    flex-shrink: 0;
}

.product-content {
    flex: 1;
}

This design gives Virtualize a reasonable initial estimate while allowing the content to grow when additional information is displayed.

Final Takeaway

Virtualize<TItem> is one of the most useful Blazor components for applications that display large collections, but virtualization has historically been easiest when every item had roughly the same height.

.NET 11 improves this area significantly by allowing Virtualize to adapt to measured item sizes and better preserve the user's viewport when content changes size.

The most important thing is not to treat variable-height support as a reason to ignore layout design.

A reliable virtualized list should still:

For a simple list, this may be enough:

<Virtualize Items="Products"
            ItemSize="90"
            Context="product">
    <ProductCard @key="product.Id"
                 Product="product" />
</Virtualize>

For a more demanding application, combine virtualization with server-side data loading and predictable UI dimensions.

The major improvement in .NET 11 is that variable-height content no longer has to be treated as an automatic reason to abandon Virtualize. The component is better equipped to handle real-world interfaces where content changes after rendering, while still providing the DOM and rendering benefits of virtualization.