Understanding and Controlling the Blazor WebAssembly Startup Process

There are a lot of things going on in the background, when a Blazor WebAssembly application is being started. In some cases you might want to take a bit more control over that process. One example might be the wish to display a loading screen for applications that take some time for initial preparation, or when users are on a slow internet connection. However, in order to control something, we need to understand what is happening first. This article takes you down the rabbit hole of how a Blazor WASM application starts up.

In this article:

sg
Sebastian Gingter is architect consultant and loves to explain things at Thinktecture. He focuses on Generative AI as well as on backends with ASP.NET Core.

Understanding the Blazor WebAssembly startup

Initial page load

As the very first step, the browser is pointed to the web server that delivers our Blazor WebAssembly (WASM) application, and so it initiates a GET request for the entry page of our Single Page Application (SPA) to the server. This could be either a simple web server hosting static files (the publish output of our WASM app), or this could be an ASP.NET Core host, which is capable of dynamically pre-rendering the initial page.

Note: Prerendering is a complex topic in itself, so this article will not go into detail about Blazor WASM prerendering. It will only be mentioned where required to get a better understanding of what’s going on.

In any case, the HTML of our main page, which comes from the static index.html or typically a dynamic host.razor file in case of server-side prerendering, is sent to the browser. The browser now does its browser thing and starts to do multiple things at the same time: First of all, it will start to parse the HTML into the Document Object Model (DOM). When it encounters links to external resources it will go on and fetch these. These are initially the CSS stylesheets, JavaScript files, web fonts and images that are referenced in the head section of the page. As soon as there are no more requests open that block the actual rendering (especially CSS files), the browser starts to display the contents of the index file.

At this point it is important to note that this is exactly the reason that the is located at the end of the body-Tag of our HTML page: The browser is already showing something, as the blazor.webassembly.js starts to initiate the heavy lifting of bringing our Blazor WASM application to life after the very first render.

The very first render

During the initial rendering the browser only shows the still “dead” HTML that the browser received from the server. This is – for now – the placeholder content in the <div id="app">…</div> tag, which will later on be the place where our Blazor application renders. In case of a normal Blazor WASM application, we can place any content here that shows an indication that the application is still loading. When we use server-side prerendering, the placeholder content in that div is of course the initial server-side rendered content of our main application component, so it might not to occur to the user that our application isn’t really alive yet.

The blazor.webassembly.js script

When the browser parsing reaches the end of the body, it will start fetching the blazor.webassembly.js script – and of course execute it. This will setup a window.Blazor object within the JavaScript engine of our single page application, and configure it for usage with Blazor WebAssembly and then start the “boot” process.

The boot process now does some really elaborate things:

First of all, it fetches a configuration file for our application: The blazor.boot.json file, which is generated as a static asset at built time. This file contains a lists of all other files that make up our actual Blazor application. You can compare this to a PWA (Progressive Web App) manifest file describing all resources required to run the PWA (see also our PWA cheatsheet from my colleague Christian Liebel). After reading this file, the main script goes on and fetches all other files that are listed in the blazor.boot.json.

Besides that, the script hooks up all interaction points for the Blazor JSInterop mechanics, all callbacks for the render methods as well as the event bridge that sends all user UI interaction with the DOM over to the WASM side. So everything is being hooked up and prepared for the Blazor application to access the JavaScript side and vice versa.

JavaScript Initializers: beforeStart When your application or a library you included in your project provides JavaScript initializers, this is the time they are being called. So the blazor.webassembly.js will fetch all provided JavaScript files that match the pattern {AssemblyName}.lib.module.js in the wwwroot folder for all assemblies that are part of your project, and if this module exports a JavaScript function called beforeStart(options, extensions), this will be invoked now.

Starting .NET in the Browser

One file that is also listed in the blazor.boot.json is the dotnet.wasm file. After this file is downloaded, the boot script uses the browsers WebAssembly APIs (see also: https://caniuse.com/?search=WebAssembly) to initialize a new WASM module and “compile” the dotnet.wasm binary code into a WASM module object, so that it can be executed.

This dotnet.wasm file is – at the moment – a huge chunk of the Mono runtime that is compiled to the WebAssembly binary format. It really is a complete .NET runtime, capable of interpreting and executing .NET IL code. What’s missing from the actual Mono runtime we know from desktop applications or servers, is the part that accesses underlying operating system APIs that are not available in the browser, i.e. certain parts of the network stack (opening listeners on sockets etc.). Also, some parts like the HttpClient object are replaced with JSInterop calls that make use of the browsers fetch API instead of doing a direct http request from a TCP socket, as this is not (yet?) possible from within the WASM runtime.

The blazor.boot.json lists every file our application needs. This of course includes the aforementioned dotnet.wasm file, but also all other .NET assemblies required to run the application. That of course contains our applications main assembly, and also all other assemblies we depend upon (i.e. System.Text.Json, Fluxor, Serilog etc.). These might also be bundled into a single big file to download, depending on whether you use packing or not.

Note: Under certain network conditions a firewall might block the downloading of “executable binaries” – just like our .dll’s. Especially in business environments where more modern firewalls are used, these might even inspect file bundles and potentially find a suspicious executable header in there – and then block the download. If the browser can’t download all assemblies and or bundles that are listed in the blazor.boot.json, because some of these get blocked by network infrastructure, the startup process will fail.

The Mono runtime in WASM

Now, after the browser has successfully compiled the WASM module and passed the handle to that module back to the blazor.webassembly.js, the script continues by actually initializing and launching the Mono runtime.

In the next step, after the other .NET assemblies listed in the blazor.boot.json file are downloaded, they are passed to the Mono runtime module as dependencies, until all required files are available within the WASM module.

After the preparation and initialization is completed, it is time to actually launch our Blazor WASM application. For that the entryAssembly is passed on to the already living Mono runtime to be executed. Mono then searches for an entry point in the entry assembly, this usually is the public static async Task Program.Main(string[] args) of our Blazor WebAssembly app, and runs this in an asynchronous fashion.

Note: At this point, the internal “boot” method of our blazor.webassembly.js is almost completed and the execution of further stuff on the JavaScript event loop could resume at this point. However, our Blazor Program.Main method is the very next piece of code on the loop, and so the boot promise will only be resolved after our .NET program “yields” for the first time, meaning execution switches to the next task by using await to wait for something else in an asynchronous way.

When WebAssembly will support multithreading in more browser implementations in the near future, and when Mono is adopted to make use of WASM threads, this could then be truly asynchronous and multithreaded. For now, there is a single thread that jumps in between JavaScript and WASM events to process in a combined event loop.

JavaScript Initializers: afterStarted

The very last thing our Blazor WASM boot method does is calling the initializers again. Just as with the beforeStart() JavaScript initializers, you and your included libraries can export an afterStarted(blazor) function in the {AssemblyName}.lib.module.js file. Now that our Blazor application is actually running, all of these functions are being called.
Note: The order is calling the entry point and as soon at this yields (not returns!), immediately invoking the afterStarted() methods. Since your Main method could yield before the WASM application host is actually being built and run (see below), you should not yet rely on a completely initialized Blazor host at this time.

The Blazor application main method

Within the Main method of a Blazor application, there are typically two distinct steps:
  1. The WebAssemblyHostBuilder is created and the application is set up. Here the DI container is configured and the root components are mapped to the corresponding HTML elements. This will tell Blazor which components to initialize when it starts and, also very important, where the dynamically rendered content of these components should be placed within the initially delivered index HTML file.
  2. After the builder is set up, the actual host object is built from it and finally executed: await builder.Build().RunAsync();

When the Blazor host runs

As soon as the Blazor host starts, it creates a DI scope for the applications lifetime and from that it creates an instance for each of the Blazor Components registered as root components. Now Blazor does its thing: These root components are then put through the Blazor Component lifecycle. This means that the parameters are set, their dependencies are injected, the initialize method is being called and then the components are rendered. It also means that complex components like the RouteView now determine what Razor component to render and/or an AuthorizeView evaluates what child components may be displayed – and these are subsequently also created, initialized and rendered.

The rendered HTML is then transferred through the JSInterop bridge into the JS part and the Blazor JS part then replaces the inner HTML of the tags linked to the root components, mainly the <div id="app">…</div> part, with the new, dynamically rendered content.

Components are (finally) alive

Its only now, after all of the previous steps have been completed successfully, that your application is ready and happy to react to the first user input.

Differences to Blazor Server and Blazor Hybrid / MAUI

In Blazor Server and Blazor Hybrid / MAUI, the window.Blazor object is initialized differently: Instead of creating a WASM module with the Mono .NET Runtime in it and starting our .NET Blazor application within this runtime, these scripts set up a communication channel to an already running .NET Runtime at a different location. In case of Blazor Server this is a SignalR connection to a new server-side circuit. In case of Blazor Hybrid it will hook up an extremely fast IPC channel between the Blazor WebView that is rendering the web application, and the .NET application that is hosting both the WebView and the actual Blazor components.

The communication between the Blazor application and the DOM in the Browser windows stays the same, only the transport on how the DOM inputs and events are passed to the .NET part and how the dynamically rendered content is passed back to the DOM differs between WASM JS/Interop, SignalR and direct IPC of the WebView to the Blazor .NET part.

Kostenloses Whitepaper: ASP.NET Core Blazor WebAssembly

Christian Weyer hat zum Thema “ASP.NET Core Blazor WebAssembly – das SPA-Framework für .NET-Entwickler?” ein Whitepaper erstellt, in dem er alles Wissenswerte zusammengefasst hat.

Melden Sie sich kostenlos zu unserem Newsletter an, um das Whitepaper per E-Mail zu erhalten.

Controlling the Blazor WASM startup

As you can see, there is a lot going on until a Blazor WASM application is ready for the user to interact with. And of course it is also quite natural that at times you want a little bit more control over that.

Custom loading screen

The first and easiest tweak that we as a developer can do, at least when we do not prerender the application on the server, is to use a custom loading screen or even animation. This simply involves changing the content of the <div id="app">…</div> element to display whatever we want during the loading phase.

JavaScript Initializers

As mentioned before, JavaScript initializers enable you to execute logic before and after the Blazor application loads. They can be used to configure the Blazor settings, customizing how the application loads and also initializing libraries, i.e. when you build a Blazor wrapper around an existing JS library and need to initialize that before first usage.

Prevent automatic startup and take manual control of the process

The next thing you might want to do, is to take control over when the actual Blazor applications startup process happens. In order to prevent an automatic starting of the process, you can add an autostart="false" attribute to the script tag that includes the blazor.webassembly.js. This will ensure that the startup part at the end of the script is not executed automatically.

One of the possible reasons to take control over the startup process is to be able to react to errors that might occur during that stage. You can execute JavaScript code directly after Blazor is running within the WASM runtime, but still before the application had time to start up, initialize and render something.

For this example we assume that some users are behind a firewall that might block assemblies from being loaded by the browser, and we want them to ask their IT staff to allow access to our app. I created an initially invisible paragraph that displays a corresponding error text. This lies within the custom loading screen, and as such it will be replaced when the application started successfully:

				
					<p id="errortext" style="visibility: hidden;">
    The Blazor host application startup encountered an error and could not be launched.
    Please ask your IT departement to allow-list this application.
</p>

				
			

To be able to show that, we need to catch errors during the startup process. As mentioned above, we first disable the automatic start by setting the autostart="false" attribute. After that we can manually call Blazor.start() whenever we see fit. This returns a normal JavaScript promise, and so we can for one execute code when the startup was successful with a .then(), and of course also .catch() any error that could occur during launching:

				
					<script src="_framework/blazor.webassembly.js" autostart="false"></script>

				
			

Note: As mentioned before, the .then() callback will be executed not before the first time we await something after our Blazor application entered the Program.Main() method. This might change when WASM multi threading support is added later on.

Do something between setup and application start

To take even more control over what happens before the Blazor application starts to render its contents, there is a third option in our Program.Main(). With the current behavior, this is the place where we can control when the .then() callback of our Blazor.start() call is executed. We simply can add an await Task.Yield(); statement here, at which our current Task (our Main method) pauses for a very brief moment and leaves room for the event loop to process the callback on the JavaScript side, before the browser thread will pick up the remainder of our Main method (the continuation of the code after that await).

Another possibility we have, is getting hold of the Blazor WebAssemblyHost object after it has been built, and do something with it before we actually start the host and have it instantiate and render our components. A use case for this could be that we are developing a PWA, and before launching our application for the first time we want to make sure that certain data is already locally available, and so we need to sync something here.

The great thing about the WebAssemblyHost is that it already has everything set up. We can access the ServiceProvider to fetch all services we need and use them to our liking, including the IJSRuntime to talk to the JavaScript side, even before our first component is instantiated:

				
					// setting up the builder before here...
var host = builder.Build();

// tell JS to display the wait-text and wait a bit
var jsRuntime = host.Services.GetRequiredService();
await jsRuntime.InvokeVoidAsync("window.displaySyncScreen");

// Example: prepare local data before launching
var syncService = host.Services.GetRequiredService();
await syncService.DownloadLocalDataAsync();

// Now start the actual Blazor app
Console.WriteLine("Starting host");
await host.RunAsync();

				
			
This first calls a JavaScript function also declared within the loading screen in our index page, which makes another prepared paragraph visible:
				
					<p id="syncscreen" style="visibility: hidden;">
    Please wait while required data is downloaded. This might take a short while…
</p>

				
			

After displaying the synchronization message, it retrieves and calls our local sync service, which will download and prepare all data in order for our PWA to be able to run offline the next time it’s started.

Summary

The Blazor WebAssembly startup process does a lot of things. While it normally goes smoothly, there are environments where even the simple part of downloading all required assemblies into the browser might fail. Also there might be use cases where we want to hook into the startup process a bit more and inform the user of what’s going on, especially when a portion like a pre-launch sync might take a minute or two.

This article explained the different parts and stages of the Blazor WASM startup process and showed you some ways to hook into that process and react to errors and execute code on JS and .NET side, before the actual Blazor application takes control of the HTML placeholders in the initially delivered website.

Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
.NET
KP-round
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
.NET
KP-round
Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023
.NET
KP-round
.NET 8 introduces a new Garbage Collector feature called DATAS for Server GC mode - let's make some benchmarks and check how it fits into the big picture.
09.10.2023