What is virtualization?
I will quote directly from the Microsoft site:
Virtualization is a technique for limiting UI rendering to just the parts that are currently visible. For example, virtualization is helpful when the app must render a long list of items and only a subset of items is required to be visible at any given time.
That has two main benefits:
- We only draw the relevant / visible part
- We only load data for the relevant / visible part in done properly
Virtualize in Action
Now how can we achieve that? That is quite simple. Imagine we have a simple card-component:
<div class="card" style="width: 18rem; height: 300px">
<img src="@ImageUrl" class="card-img-top" alt="">
<div class="card-body">
<p class="card-text">Imagedescription could be here</p>
</div>
</div>
@code {
[Parameter]
public string ImageUrl { get; set; }
protected override void OnInitialized()
{
Console.WriteLine("Called on " + DateTime.Now);
}
}
Now we can use it as follows:
@page "/"
<PageTitle>Index</PageTitle>
<div style="height:500px;overflow-y:scroll">
@foreach (var url in ImageUrls)
{
<ImageCard ImageUrl="@url"></ImageCard>
}
</div>
@code {
private readonly string[] ImageUrls = {
"https://images.unsplash.com/photo-1454496522488-7a8e488e8606?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1176&q=80",
"https://images.unsplash.com/photo-1519681393784-d120267933ba?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1465056836041-7f43ac27dcb5?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1171&q=80",
"https://images.unsplash.com/photo-1483728642387-6c3bdd6c93e5?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1176&q=80",
"https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1549880181-56a44cf4a9a5?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1547093349-65cdba98369a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1480497490787-505ec076689f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1169&q=80",
};
}
What we do is, we have 8 images which we show in a for
loop. As we can see here:
Only 2 images are visible at a time. But still we loaded all the images and rendered the cards even though they are not visible to our user. On top we can see the console output:
Called on 01/01/2022 20:51:25
Called on 01/01/2022 20:51:25
Called on 01/01/2022 20:51:25
Called on 01/01/2022 20:51:25
Called on 01/01/2022 20:51:25
Called on 01/01/2022 20:51:25
Called on 01/01/2022 20:51:25
Called on 01/01/2022 20:51:25
We can see this easily in the inspector as well:
All images are directly loaded and rendered. Now let's check how this would look like if we virtualize
the content. We only download and render the images "on-demand".
Before we dive into the code let's have a look at the inspector again:
And the console output:
Called on 01/01/2022 21:06:05
Called on 01/01/2022 21:06:05
Called on 01/01/2022 21:06:05
Called on 01/01/2022 21:06:05
Called on 01/01/2022 21:06:05
Called on 01/01/2022 21:06:19
Called on 01/01/2022 21:06:19
Called on 01/01/2022 21:06:19
Called on 01/01/2022 21:06:19
Called on 01/01/2022 21:06:19
So what do we see?
- Only the first set of cards is really loaded. Once we continue scrolling done a new batch of cards is loaded and rendered
- But also when we scroll up again the batch we downloaded and displayed earlier has to be retrieved once again
<Virtualize>
some code
Now how did we achieve that? Let's have a small look:
<div style="height:500px;overflow-y:scroll">
<Virtualize Items="@ImageUrls" OverscanCount="1" ItemSize="300">
<ImageCard ImageUrl="@context"></ImageCard>
</Virtualize>
</div>
Now let's unwrap that a bit. First we replace the for
loop with the Virtualize
component. Here the link to the official documentation.
Items
describes our enumeration. Here we passed just our list. Now the next two properties are not really necessary and I used them just for the sake of demonstration, but they can be useful anyway:
OverscanCount
: sets the numbers of items after and before the current visible area which should be rendered. If you set that number very high you unnecessarily initialize components which might not be needed anyway. To low and you see visible loading when your user scrolls down.ItemSize
: Simple speaking the size per item in pixel. Blazor on it's own can do that for you, you don't necessarily have to provide that. Blazor achieves this by simply rendering your component once.
@context
seems very magic. It holds our item. So in our example it holds one url. You can give the context a name if you wish:
<Virtualize Items="@ImageUrls" Context="@url">
<ImageCard ImageUrl="@url">
</virtualize>
For more advanced use-cases have a look here
When to use
Now we got a basic idea how to replace a for
loop with the Virtualize
component. What are typical use cases?
- Rendering a set of data items in a loop.
- Most of the items aren't visible due to scrolling. (just imagine something like endless scroll where you load all the data upfront 😄 )
- The rendered items are the same size.
Now why is the last point important? Blazor has to estimate (based on your viewport) when to load new items and when not. If you have constantly changing sizes of your items this can get very tricky and lead to problems.
ItemsProvider
Instead of giving an IEnumerable<T>
to the Virtualize
component as shown above you also have the option to defined an ItemsProvider
.
The difference here is that you have to figure out which items to load depending on the current state.
To know where you currently are blazor gives you a ItemsProviderRequest
-object to the requested delegate. Let's have a look.
First the usage:
<Virtualize ItemsProvider="@GetImages" OverscanCount="1" ItemSize="300">
<ImageCard ImageUrl="@context"></ImageCard>
</Virtualize>
So instead of the Items
property we use the ItemsProvider
property of the component, the rest stays the same.
Now to the new part, the delegate:
private async ValueTask<ItemsProviderResult<string>> GetImages(ItemsProviderRequest request)
{
await Task.Yield(); // Just to make it async
var numImages = Math.Min(request.Count, ImageUrls.Length - request.StartIndex);
var urls = ImageUrls.Skip(request.StartIndex).Take(numImages);
return new ItemsProviderResult<string>(urls, ImageUrls.Length);
}
We are just loading a batch of images from our array and return it to the provider.
Note: Don't define both Items
and ItemsProvider
as the component will throw an InvalidOperationException
.
Placeholder
Now take the last part a bit further. We loaded images from an ItemsProvider
. Normally that takes some time. To display something instead we can leverage the Placeholder
property. Note: This will only work with ItemsProvider
and not with Items
as of the time I am writing that blog post (.NET 6).
<Virtualize Items="@GetImages" OverscanCount="1" ItemSize="300">
<ItemContent>
<DelayComponent></DelayComponent>
</ItemContent>
<Placeholder>
<h2>Loading...</h2>
</Placeholder>
</Virtualize>
The typical use-case would be that you retrieve some data from a repository inside your ItemsProviderDelegate
.
Conclusion
Virtualize
is a nice way to reduce load and pressure of your application. We went from the basics to the more advanced scenarios.
If you have any questions let me know. Input and feedback is always welcomed!