I’m experimenting with Uno Platform for a while now - it’s a great piece of software and enables dotnet developers to use their existing skills to build cross-platform apps. For me it’s interesting especially for its WebAssembly capabilities, which were available even before Blazor from Microsoft was a thing.
I’m currently working on an Uno WebAssembly application where i need to display large amounts of items in collection with different templates, therefore i was investigating how i could speed up the loading of the built-in ListView
and GridView
controls. In my usecase the number of items in the ItemSource
of the collection is not predefined which means that users can have a collection with 1 item or with 99999 items. That’s the reason why i needed a solution which allows to display a specific amount of items initially and lazy-load more data on-demand.
During my research i stumbled across the Uno implementation of the NuGet package explorer which has lazy-loading functionality for the ListView
control. I adapted this approach for my application and made some changes so that the GridView
control could also support lazy-loading.
TL;DR
here is the source of the demo project:
https://github.com/nor0x/UnoLazyLoading
🦥 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
.
1 | var myScrollView = ExtendedVisualTreeHelper.GetFirstDescendant<ScrollViewer>(myGridView); |
We will add code to check for the need to lazy-loading whenever the ViewChanged
event is fired - this happens when the user scrolls the collection.
In this code we use the ExtentHeight
, VerticalOffset
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.
1 | public class PaginatedCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading |
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.
1 | 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.
1 | void LoadGrid() |
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.
1 | 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.
more more more performance 🏎️
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 documentation.