Part of a series on Blazor WASM initial load performance
Part 2: Improving Data Loading
As seen in the previous post, Prerendering can mitigate the initial page load time problem, but there are some new problems introduced by this solution. Using this technique requires loading the same data on both the server side (for initial component rendering) and client side (from the full application) during the different phases of app startup, which can cause identical data loading requests to be made multiple times from the same app instance. This can create extra load on your data sources, but also can lead to bad user experience as data already loaded on the server may then disappear from the page while the client waits for it to load.
PersistComponentState
To prevent this duplicated loading of data, the persist-component-state
tag can be added to include the data initially loaded on the server along with the client download of the app. In _Host.cshtml
that looks like this:
<div id="app">
<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />
<persist-component-state />
</div>
To take advantage of the data persistence, your components must handle adding data and then retrieving it when available. This requires injecting the appropriate service and then registering a handler method for it:
[Inject]
public PersistentComponentState ApplicationState { get; set; }
private PersistingComponentStateSubscription _stateSubscription;
protected override async Task OnInitializedAsync()
{
_stateSubscription = ApplicationState.RegisterOnPersisting(PersistPageData);
}
private Task PersistPageData()
{
return Task.CompletedTask;
}
public void Dispose()
{
_stateSubscription.Dispose();
}
Any relevant data then needs to be loaded from the state object when available, and saved into it in the handler method. Make sure to use the same key for both the read and write operations. Here I’m using the name of the service method producing the data as an easy association that is also compiler enforced.
protected override async Task OnInitializedAsync()
{
_stateSubscription = ApplicationState.RegisterOnPersisting(PersistPageData);
if (ApplicationState.TryTakeFromJson<List<NoteData>>(nameof(DataService.GetNotesFastAsync), out var cachedNotes))
{
Notes = cachedNotes;
}
else
{
Notes = await DataService.GetNotesFastAsync();
}
}
private Task PersistPageData()
{
if (Notes != null)
ApplicationState.PersistAsJson(nameof(DataService.GetNotesFastAsync), Notes);
return Task.CompletedTask;
}
Now when the page is loaded the server will initially request the data, render it in the component, and store it in state. The client will then initialize its own copy of the same component, but first check state and find the existing data already available with no additional API call back to the server needed. Client side rendering happens faster, no requests are duplicated, and identical data can be rendered in the UI, reducing visual disruptions.
Handling Slow Data Requests
Prerendering naturally requires that any data to be initially displayed is loaded at the server, but what if you also have some very slow loading data included on that first page, like a call to some external system out of your control? Waiting for the complete data set before showing anything may end up being an even worse experience than not using pre-rendering at all. Taking advantage of async requests can prevent the slow data from blocking page load, while also making the data available to the UI as soon as it becomes available. Used in combination with state persistence the user can now be served with UI and data as soon as possible.
Adding in a call to a service method with a built-in 5 second delay to my component turns my initial page load from nearly instant to more than 5 seconds.
protected override async Task OnInitializedAsync()
{
_stateSubscription = ApplicationState.RegisterOnPersisting(PersistPageData);
if (ApplicationState.TryTakeFromJson<List<CalendarEvent>>(nameof(DataService.GetCalendarSlowAsync), out var cachedEvents))
{
Calendar = cachedEvents;
}
else
{
Calendar = await DataService.GetCalendarSlowAsync();
}
// load other data...
}
I’m incorporating state persistence so would need to also add to my PersistPageData
method. If I forget to do that the user experience would be a long initial wait for page load, followed by the page then resetting back to empty with no data as the client takes over, then the same data re-appearing later. This is exactly the situation we want to avoid.
To get that initial rendering time back down, I want to change OnInitializedAsync
so that it makes the initial call to load the data, but doesn’t await the result. This will let the page render without the data and allow showing most of the page with some kind of placeholder indicating other data is still loading.
protected override async Task OnInitializedAsync()
{
_stateSubscription = ApplicationState.RegisterOnPersisting(PersistPageData);
if (ApplicationState.TryTakeFromJson<List<CalendarEvent>>(nameof(DataService.GetCalendarSlowAsync), out var cachedEvents))
{
Calendar = cachedEvents;
}
else
{
// not awaited
LoadCalendarData();
}
// load other data...
}
private async void LoadCalendarData()
{
Calendar = await DataService.GetCalendarSlowAsync();
StateHasChanged();
}
It’s important to include a call to StateHasChanged
here so that the new data actually gets rendered when it does finally load. Depending on your client state management strategy (i.e. something like Fluxor) you may get this for free.
So now, even with a 5 second wait for data I can still get a very fast initial page load from Prerendering, with a placeholder showing until the missing data becomes available to the client.
One important note is that with my current setup, although I am only loading data into the component once, the request for the data was actually made twice because the server side did not wait for a response and so discarded the result rather than adding it to persisted state. If this is an important consideration you may want to make additional changes to avoid starting the load of that data at all on the server side using something like this method.
Next: Authentication In Prerendering
Code Versions
Example code is using .NET 7.0 with these library versions:
- Microsoft.AspNetCore.Authentication.OpenIdConnect 7.0.2