Implementing Lazy Loading Functionality in Uno WebAssembly Applications

No matter the purpose of your application or website, performance is essential to a great user experience. Slow loading apps and webpages will turn users away almost immediately. Fortunately, Lazy Loading is a technique used to optimize load time by only loading required content at first, and loading any remaining page content once the user needs it. 

In this article, Johnny guides us through the steps required to implement the Lazy Loading functionality for ListView and Gridview controls in Uno Platform WASM applications.

Lazy Loading

Lazy Loading or incremental loading is a strategy to delay the loading of objects until they are actually needed. This can help reducing resources used by the application and increase loading times for the item collection, because only parts of the data are loaded initially.

ScrollView

For my usecase the trigger for loading more items is a certain scrolling threshold in the ListView and GridView control. To get the current scroll position we need to somehow access the ScrollView within the ListViewBase which is the base class for ListView and GridView. We will use the VisualTreeHelper class to get information about nodes in the visual tree and get a reference to the inner ScrollView of the ListViewBase

				
					var myScrollView = ExtendedVisualTreeHelper.GetFirstDescendant<ScrollViewer>(myGridView);

myScrollView.ViewChanged += async (s, e) =>
{
    if (myGridView.ItemsSource is not ISupportIncrementalLoading source) return;
    if (myGridView.Items.Count > 0 && !source.HasMoreItems) return;
    if (GetIsIncrementallyLoading(myGridView)) return;

    if (((myScrollView.ExtentHeight - myScrollView.VerticalOffset) / myScrollView.ViewportHeight) - 1.0 <= loadingThreshold)
    {
        SetIsIncrementallyLoading(myGridView, true);
        await source.LoadMoreItemsAsync(1);
    }
}

				
			

We will add code to check for the need for lazy-loading whenever the ViewChanged event is fired – this happens when the user scrolls the collection.

In this code, we use the ExtentHeightVerticalOffset and ViewportHeight of the ScrollView to check if more than half of the scroll area was already scrolled, which will initiate the loading of more items.

ItemsSource

Now we have the trigger when we want to load more items into our collection. For the actual fetch of the data we will use a special collection as the ItemsSource of your ListViewBase control.

				
					public class PaginatedCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading
{
    public delegate Task<T[]> Fetch(int start, int count);

    private readonly Fetch _fetch;
    private int _start, _pageSize;

    public PaginatedCollection(Fetch fetch, int pageSize)
    {
        _fetch = fetch;
        _start = 0;
        _pageSize = pageSize;
    }

    public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
    {
        return Task.Run<LoadMoreItemsResult>(async () =>
        {
            var items = await _fetch(_start, _pageSize);
            await CoreApplication.MainView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
            {
                foreach (var item in items)
                {
                    Add(item);
                    if (Count > _pageSize)
                    {
                        //hack to give the UI time for layout udpates
                        await Task.Delay(20);
                    }
                }
            });

            _start += items.Length;

            return new LoadMoreItemsResult() { Count = (uint)items.Length };
        }).AsAsyncOperation();
    }

    public bool HasMoreItems => true;
}
				
			

The PaginatedCollection derives from ObservableCollection which already has mechanisms for UI updates via databinding if the collection changes. I have added a Fetch object to the class which can be any method responsible for getting more data (i.e. a call to a backend service). The fetch is called in LoadMoreItemsAsync which itself is triggered if the scrolling threshold is reached (see extension classes above). The collection has a _pageSize field which defines the number of items to be loaded.

The last part for using lazy-loading is to set the ItemsSource of the control to a PaginatedCollection and add the AddIncrementallyLoadingSupport="True" attribute to the control.

				
					local:ListViewExtensions.AddLazyLoadingSupport="True"
				
			

Let’s create an Items collection and set the page size accordingly. For the demo app i’m simulating a data fetch by adding a Task.Delay to the method.

				
					void LoadGrid()
{
    Items = new PaginatedCollection<Item>(
        async (start, size) =>
        {
            var response = await FetchItems(start, size);
            return response;
        },
        pageSize: 25
    );
}

async Task<Item[]> FetchItems(int start, int size)
{
    //simulates some work to get new items
    await Task.Delay(200);
    return _allItems.Skip(start).Take(size).ToArray();
}
				
			

Demo

I have tested the lazy-loading solution with ListView and GridView controls and in both cases the loading performance and responsiveness of the collection could be increased significantly. I also found out that ListView seems to be optimized already and didn’t benefit as much as GridView. I’m comparing the lazy-loading approach to controls with standard ObservableCollection where all items are loaded initally. The Item model is just a record with a name and a random image url.

				
					public record Item(string Name, string Image);
				
			

These values are displayed in a StackPanel within the DataTemplate of the ItemsControl. For testing i can set the count of the collection and trigger loading of each ItemsControl separately.

You can see in the recording that the regular GridView even blocks the UI when the loading starts. Note that this sample is running in InterpretedAndAOT mode – more info below.

Runtime Execution Modes

This post is just about lazy-loading for ListView and GridView – but there is more you can do to improve the performance of Uno WebAssembly apps. First of all, it makes a big difference what’s the runtime execution mode of your application. It can be set via the WasmShellMonoRuntimeExecutionMode element in the .csproj file of the WebAssembly project. There are three different values for the element.

  • Interpreter
  • InterpreterAndAOT
  • FullAOT

Interpreter is the default value and has the fastest build times but the slowest performance, since all the code is interpreted during runtime. For better perfomance you want to try some form of AOT compilation for your project. You could go with FullAOT to compile all assemblies to wasm binaries Ahead of Time. This has the best performance in regards to code execution but produces the largest application package. For my usecase the InterpreterAndAOT mode is a nice compromise, it allows mix AOT compilation and interpreted code.

ListViewBase Performance

Most of the tips and tricks for ListViewBase are explained in the |documentation. Depending on your usecase it might help to experiment with List vs. ObservableCollection as the type of the ItemsSource. It’s also worth noting that you can set your custom styles for the ItemTemplate of the ItemsControl. Especially when using GridView, replacing the default GridViewItem style can increase performance a lot, this is also mentioned in the Uno Platform documentation.

Thank you Joachim Leonfellner

A big thank you to @Johnny for putting this in-depth guide together and letting us share it with the wider Uno Platform community. You can find the original article and more on his blog.

Next Steps

If you are new to Uno Platform, the best way to get started is to follow our official getting started guide. (5 min to complete)

For better discoverability and ease of adopting Uno Platform we have brushed up and published various working examples for Uno Platform, ranging from small single-feature samples to larger showcase applications. Check it all out and expect many more to come in the future.

Share this post: