Lazy load components with Blazor - Virtualize in Action

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:

  1. We only draw the relevant / visible part
  2. 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:

preview

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:

NoVirtualize

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:

Virtualize

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!

2
An error has occurred. This application may no longer respond until reloaded. Reload x