Part of a series on Blazor WASM initial load performance

  1. Server Prerendering
  2. Improving Data Loading
  3. Authentication In Prerendering

Part 1: Server Prerendering

Blazor WASM performance in .NET 7 can be improved using a variety of techniques but there are tradeoffs to each. AOT (Ahead Of Time) compilation can improve runtime performance in the browser, but does so at the cost of long compile times and increased app download size.

<PropertyGroup>
  <RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>

Unfortunately, AOT is not helpful during initial page load because the primary impact at startup is the increase to the app download size. In cases where initial page load time is important the size penalty may not be acceptable even if the improved performance later on is desirable. A different solution is needed.

Prerendering

To specifically improve initial page load, Server Side Prerendering can be used. With Prerendering enabled, first requests are handled by loading and rendering components on the server, sending a fully rendered page that can be displayed immediately, and then replacing it with the full application after it has finished downloading in the background.

There are multiple steps needed to enable pre-rendering, changing files on both server and client, detailed in the docs here. Following these changes my demo _Host.cshtml looks like this:

@page "/"
@using BlazorAuth.Client
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Mvc.TagHelpers
@namespace BlazorAuth.Server.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorAuth.Client.styles.css" rel="stylesheet" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <component type="typeof(HeadOutlet)" render-mode="WebAssemblyPrerendered" />
</head>
<body>
    <div id="app">
        <component type="typeof(App)" render-mode="WebAssemblyPrerendered" />

        <persist-component-state />
    </div>

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

Data Services

To use Prerendering, there is also some work to do to create data services that can be injected on both server and client side to load the same data, but this requirement can also be an advantage as it tends to encourage better architecture. For my example I’m using a service interface which has both client and server implementations.

public interface ILoadDataService
{
    Task<List<NoteData>?> GetNotesFastAsync();
    Task<List<CalendarEvent>?> GetCalendarSlowAsync();
}

The client service implementation delegates requests to the server APIs:

public class LoadDataClientService : ILoadDataService
{
    private readonly HttpClient _httpClient;

    public LoadDataClientService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<List<NoteData>?> GetNotesFastAsync()
    {
        return await _httpClient.GetFromJsonAsync<List<NoteData>>("Notes");
    }

    public async Task<List<CalendarEvent>?> GetCalendarSlowAsync()
    {
        return await _httpClient.GetFromJsonAsync<List<CalendarEvent>>("Calendar");
    }
}

On the server, the data is just directly generated and returned. In a real application, this could be coming from a database, other APIs, etc.

public class LoadDataServerService : ILoadDataService
{
    public async Task<List<NoteData>?> GetNotesFastAsync()
    {
        return GetNotes();
    }

    public async Task<List<CalendarEvent>?> GetCalendarSlowAsync()
    {
        await Task.Delay(5000);

        return GetEvents();
    }
    
    //Additional data generation implementation
}

The same service is also injected into the controllers on the server side that serve the requests from the client:

private readonly ILoadDataService _loadDataService;

[HttpGet]
public async Task<List<NoteData>?> Get()
{
    return await _loadDataService.GetNotesFastAsync();
}

Now that the basics are set up, the following post will dive into some potential issues that our Prerendering changes introduce.

Next: Improving Data Loading

Code Versions

Example code is using .NET 7.0 with these library versions:

  • Microsoft.AspNetCore.Components.WebAssembly 7.0.2
  • Microsoft.AspNetCore.Components.WebAssembly.Server 7.0.2