.NET 8 Blazor component visibly loading twice? Check your prerendering

December 12, 2023 · 5 minute read · Tags: blazor

Data loading twice in your Blazor .NET 8 app?

Chances are it’s down to prerendering.

By default in .NET 8 your interactive components are rendered twice:

Once on the server (prerendering), where HTML is returned so the browser can show something nice and quickly.

Then your component will be rendered again using your preferred interactive render mode (server or WASM).

The problem is, if you’re fetching data in your component, it’s going to be fetched for both renders.

You can often see this in the UI, with a visible flash as the prerendered content is replaced with the “new data” when your component is rendered for a second time.

So what to do about it? Turns out there are two main options at your disposal:

  • Disable prerendering
  • Persist state between the first and second renders

Disable prerendering

The quick and easy option is to disable prerendering for your component.

@page "/"
@rendermode @(new InteractiveServerRenderMode(false))

<h1>Hello</h1>

Here we’ve specified that the render mode for this component is Interactive Server. The false parameter switches prerendering off (for this component).

Now it will render once, and once only, using Interactive Server render mode.

Persist state between renders

But of course prerendering can be a useful thing.

It ensures your users see something in the browser even while Blazor Server/WASM loads up in the background.

So the other option is to persist the state you fetch during the first render (prerender) and use it when your component renders again (using one of .NET 8’s interactive modes).

The key to this is Blazor’s PersistentComponentState to both store, and then retrieve your component’s important state.

Take this component which greets the logged in user.

We’re fetching the user’s name from a UserService service (which, in a real app would likely identify the logged in user, and fetch their details from a database.)

Profile.razor

@page "/"
@inject PersistentComponentState ApplicationState
@inject UserService UserService
@implements IDisposable

Welcome @_username
@code {

    private PersistingComponentStateSubscription _subscription;
    private string? _username;

    protected override async Task OnInitializedAsync()
    {
        _subscription = ApplicationState.RegisterOnPersisting(Persist);

        var foundInState = ApplicationState
            .TryTakeFromJson<string>("userName", out var userName);

        _username = foundInState
            ? userName
            : UserService.GetUserName();
    }

    private Task Persist()
    {
        ApplicationState.PersistAsJson("userName", _username);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _subscription.Dispose();
    }

}

There’s a little bit going on here, so let’s break it down.

First, we inject an instance of the PersistingComponentState service, and store it in a field called ApplicationState.

In OnInitializedAsync we then use its RegisterOnPersisting method.

This enables us to register a callback which Blazor can invoke at the right time to store our state (just before it returns the rendered HTML to the client).

In this case we register our Persist method as a callback.

Here’s the flow for that first part of the process:

First Render (prerender)

sequenceDiagram participant Browser box rgb(255,238,204) Server participant Server as Profile
Component participant UserService participant PersistentComponentState end Browser ->>+ Server: HTTP Request /Profile Server ->>+ UserService: Fetch user details UserService -->>+ Server: User Details note over Server: Render Server ->> PersistentComponentState: Store user details Server ->> Browser: HTML

When the component is rendered again (this time using Blazor Server or WASM) we can fetch and use this persisted state.

That’s what the rest of the code in OnInitializedAsync takes care of.

protected override async Task OnInitializedAsync()
{
    ...

    var foundInState = ApplicationState
        .TryTakeFromJson<string>("userName", out var userName);

    _username = foundInState
        ? userName
        : UserService.GetUserName();
}

Here we try to fetch the current value of UserName from our persistent component state (ApplicationState).

If we find it, happy days, we can use take its value and assign it to our _username field.

If we don’t find a value in ApplicationState we’re probably rendering this component for the first time.

In that case we want to look up the user’s details using UserService then assign the resulting value to _username

Because we registered Persist as a callback via ApplicationState.RegisterOnPersisting we can be sure it will be invoked at the right time to store the value of _username (which we can then access in subsequent renders).

private Task Persist()
{
    ApplicationState.PersistAsJson("userName", _username);
    return Task.CompletedTask;
}

When our component renders the second time, the following code will retrieve the persisted state:

var foundInState = ApplicationState
    .TryTakeFromJson<string>("userName", out var userName);

Meaning we can use that, and skip the call to UserService

Second Render (Server Interactive)

sequenceDiagram participant Browser box rgb(255,238,204) Server participant Server as Profile
Component participant PersistentComponentState participant UserService end Browser ->>+ Server: Render Profile
(Server Interactive) Server ->> PersistentComponentState: Try Get User Details PersistentComponentState -->> Server: Persisted User Details note over Server: Render Server ->> Browser: DOM Diff

Properly disposing of the subscription

Finally, it’s important to clean up the subscription when our component is disposed.

Without this we’ll have subscriptions continue to exist even after the component that needed it has long since been disposed off.

This could cause a memory leak in your application, with each lingering subscription consuming resources long after it’s needed.

To solve that we can make our component implement IDisposable.

This requires us to implement a Dispose method which Blazor will invoke when the component is disposed.

There we can call the Dispose method on our _subscription field to ensure it is cleaned up at the same time as our component.

Summary

Prerendering makes for a faster initial load for your users.

But it can also lead to your component fetching data twice (once during prerendering, and again when rendered using one of the interactive render modes in .NET 8).

You can switch prerendering off if you don’t need it.

Alternatively, you can persist state during the first render, and use it during the second, to save making multiple calls to fetch the exact same data.

Persisting the state between renders adds more ‘plumbing’ code to your component, but ensures you only fetch data once, and avoids the user seeing that ‘flash’ in the UI as your component updates with the new data (during the second render).

Next up

How to upload a file with Blazor SSR in .NET 8?
How to handle file uploads without using an interactive render mode?
3 simple design tips to improve your Web UI
Spruce up your features
The quickest way to integrate PayPal checkout with Blazor SSR in .NET 8
JavaScript Interop works differently with Blazor Server-side rendering