Problem
Microsoft documentation provides an example of running application-wide initialization logic in Program.cs prior to rendering content:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...
var host = builder.Build();
var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync();
await host.RunAsync();
}
Notice that host.RunAsync()
only runs after weatherService.InitializeWeatherAsync()
completes; the application startup time increases by the duration of the initialization logic (even if it runs asynchronously)!
I have created a webpage demonstrating the delay (source code), where InitializeWeatherAsync
looks like this:
public async Task InitializeWeatherAsync()
{
Console.WriteLine("WeatherService: Initialization started");
await Task.Delay(10000); // simulate asynchronous work that takes 10s to complete
Console.WriteLine("WeatherService: Initialization completed");
}
Observe the following sequence of events when loading the webpage:
- Application starts up; Browser shows “Loading…”
- Console prints “WeatherService: Initialization started”
- After 10s, Console prints “WeatherService: Initialization completed”
- Browser renders application UI
This is bad for User Experience as the user is stuck at the loading page for 10s before being able to see anything meaningful and interact with the application.
Solution
If you really have to perform additional initialization logic before rendering content, then you are out of luck.
However, if that’s not necessary because the user can meaningfully interact with the application before the additional initialization logic completes, then there’s 2 ways about it:
-
Run the asynchronous method without
await
-ing itIf
weatherService.InitializeWeatherAsync()
is notawait
-ed, then the thread will not wait for it to return before runninghost.RunAsync()
. You can also useContinueWith
to process the result or perform exception handling etc.This solution however, still introduces delay to application startup. As of today, WASM runs in a single-thread, so the additional initialization logic still takes up CPU cycles that would have been otherwise used to startup the application (especially so if the additional initialization logic contains a lot of code that are executed synchronously).
-
Callback
This implementation references the pattern described in Microsoft’s documentation on In-memory State Container Service. In summary:
-
Create a State Container with
bool _isFirstRendered
and a delegateevent Action OnChange
. -
Update
WeatherService
’s constructor to addInitializeWeatherAsync()
toOnChange
. -
Create a parent Component that all Components inherit from, with the following code:
protected override void OnAfterRender(bool firstRender) { if (firstRender) { FirstRenderStateContainer.SetRendered(); // sets `_isFirstRendered` to true and invokes methods in `OnChange` } }
The results of this implementation can be found on this webpage (source code), with the startup time shortened by 10s compared to the earlier example, since the additional initialization logic is executed after the UI has been rendered rather than before.
Again, observe the output on Console for the sequence of events.
-
Conclusion
Mobile devices with limited processing power like mine (Samsung Galaxy A31) takes about 4 seconds to load a barebone Blazor WASM application, which is really long. Therefore, reduce additional initialization logic in Program.cs as far as possible.
Also, if you have a better idea on how this problem can be resolved, do create an issue here and let’s discuss about it!