Edit

Share via


Prerender ASP.NET Core Razor components

Note

This isn't the latest version of this article. For the current release, see the .NET 9 version of this article.

Important

This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

For the current release, see the .NET 9 version of this article.

This article explains Razor component prerendering scenarios for server-rendered components in Blazor Web Apps.

Prerendering is the process of initially rendering page content on the server without enabling event handlers for rendered controls. The server outputs the HTML UI of the page as soon as possible in response to the initial request, which makes the app feel more responsive to users. Prerendering can also improve Search Engine Optimization (SEO) by rendering content for the initial HTTP response that search engines use to calculate page rank.

Persist prerendered state

Without persisting prerendered state, state used during prerendering is lost and must be recreated when the app is fully loaded. If any state is created asynchronously, the UI may flicker as the prerendered UI is replaced when the component is rerendered.

Consider the following PrerenderedCounter1 counter component. The component sets an initial random counter value during prerendering in OnInitialized lifecycle method. After the SignalR connection to the client is established, the component rerenders, and the initial count value is replaced when OnInitialized executes a second time.

PrerenderedCounter1.razor:

@page "/prerendered-counter-1"
@rendermode @(new InteractiveServerRenderMode(prerender: true))
@inject ILogger<PrerenderedCounter1> Logger

<PageTitle>Prerendered Counter 1</PageTitle>

<h1>Prerendered Counter 1</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;

    protected override void OnInitialized()
    {
        currentCount = Random.Shared.Next(100);
        Logger.LogInformation("currentCount set to {Count}", currentCount);
    }

    private void IncrementCount() => currentCount++;
}

Run the app and inspect logging from the component. The following is example output.

Note

If the app adopts interactive routing and the page is reached via an internal enhanced navigation, prerendering doesn't occur. Therefore, you must perform a full page reload for the PrerenderedCounter1 component to see the following output. For more information, see the Interactive routing and prerendering section.

info: BlazorSample.Components.Pages.PrerenderedCounter1[0]
currentCount set to 41
info: BlazorSample.Components.Pages.PrerenderedCounter1[0]
currentCount set to 92

The first logged count occurs during prerendering. The count is set again after prerendering when the component is rerendered. There's also a flicker in the UI when the count updates from 41 to 92.

To retain the initial value of the counter during prerendering, Blazor supports persisting state in a prerendered page using the PersistentComponentState service (and for components embedded into pages or views of Razor Pages or MVC apps, the Persist Component State Tag Helper).

To preserve prerendered state, use the [SupplyParameterFromPersistentComponentState] attribute to persist state in properties. Properties with this attribute are automatically persisted using the PersistentComponentState service during prerendering. The state is retrieved when the component renders interactively or the service is instantiated.

By default, properties are serialized using the System.Text.Json serializer with default settings. Serialization isn't trimmer safe and requires preservation of the types used. For more information, see Configure the Trimmer for ASP.NET Core Blazor.

The following counter component persists counter state during prerendering and retrieves the state to initialize the component:

  • The [SupplyParameterFromPersistentComponentState] attribute is applied to the CounterState type (State).
  • The counter's state is assigned when null in OnInitialized and restored automatically when the component renders interactively.

PrerenderedCounter2.razor:

@page "/prerendered-counter-2"
@inject ILogger<PrerenderedCounter2> Logger

<PageTitle>Prerendered Counter 2</PageTitle>

<h1>Prerendered Counter 2</h1>

<p role="status">Current count: @State?.CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [SupplyParameterFromPersistentComponentState]
    public CounterState? State { get; set; }

    protected override void OnInitialized()
    {
        if (State is null)
        {
            State = new() { CurrentCount = Random.Shared.Next(100) };
            Logger.LogInformation("CurrentCount set to {Count}", 
                State.CurrentCount);
        }
        else
        {
            Logger.LogInformation("CurrentCount restored to {Count}", 
                State.CurrentCount);
        }
    }

    private void IncrementCount()
    {
        if (State is not null)
        {
            State.CurrentCount++;
        }
    }

    public class CounterState
    {
        public int CurrentCount { get; set; }
    }
}

When the component executes, CurrentCount is only set once during prerendering. The value is restored when the component is rerendered. The following is example output.

Note

If the app adopts interactive routing and the page is reached via an internal enhanced navigation, prerendering doesn't occur. Therefore, you must perform a full page reload for the component to see the following output. For more information, see the Interactive routing and prerendering section.

info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
CurrentCount set to 96
info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
CurrentCount restored to 96

In the following example that serializes state for multiple components of the same type:

  • Properties annotated with the [SupplyParameterFromPersistentComponentState] attribute are serialized and deserialized during prerendering.
  • The @key directive attribute is used to ensure that the state is correctly associated with the component instance.
  • The Element property is initialized in the OnInitialized lifecycle method to avoid null reference exceptions, similarly to how null references are avoided for query parameters and form data.

PersistentChild.razor:

<div>
    <p>Current count: @Element.CurrentCount</p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</div>

@code {
    [SupplyParameterFromPersistentComponentState]
    public State Element { get; set; }

    protected override void OnInitialized()
    {
        Element ??= new State();
    }

    private void IncrementCount()
    {
        Element.CurrentCount++;
    }

    private class State
    {
        public int CurrentCount { get; set; }
    }
}

Parent.razor:

@page "/parent"

@foreach (var element in elements)
{
    <PersistentChild @key="element.Name" />
}

In the following example that serializes state for a service:

  • Properties annotated with the [SupplyParameterFromPersistentComponentState] attribute are serialized during prerendering and deserialized when the app becomes interactive.
  • The AddPersistentService method is used to register the service for persistence. The render mode is required because the render mode can't be inferred from the service type. Use any of the following values:
    • RenderMode.Server: The service is available for the Interactive Server render mode.
    • RenderMode.Webassembly: The service is available for the Interactive Webassembly render mode.
    • RenderMode.InteractiveAuto: The service is available for both the Interactive Server and Interactive Webassembly render modes if a component renders in either of those modes.
  • The service is resolved during the initialization of an interactive render mode, and the properties annotated with the [SupplyParameterFromPersistentComponentState] attribute are deserialized.

Note

Only persisting scoped services is supported.

CounterService.cs:

public class CounterService
{
    [SupplyParameterFromPersistentComponentState]
    public int CurrentCount { get; set; }

    public void IncrementCount()
    {
        CurrentCount++;
    }
}

In Program.cs:

builder.Services.AddPersistentService<CounterService>(RenderMode.InteractiveAuto);

Serialized properties are identified from the actual service instance:

  • This approach allows marking an abstraction as a persistent service.
  • Enables actual implementations to be internal or different types.
  • Supports shared code in different assemblies.
  • Results in each instance exposing the same properties.

As an alternative to using the declarative model for persisting state with the [SupplyParameterFromPersistentComponentState] attribute, you can use the PersistentComponentState service directly, which offers greater flexibility for complex state persistence scenarios. Call PersistentComponentState.RegisterOnPersisting to register a callback to persist the component state during prerendering. The state is retrieved when the component renders interactively. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown.

The following counter component example persists counter state during prerendering and retrieves the state to initialize the component.

PrerenderedCounter3.razor:

@page "/prerendered-counter-3"
@implements IDisposable
@inject ILogger<PrerenderedCounter3> Logger
@inject PersistentComponentState ApplicationState

<PageTitle>Prerendered Counter 3</PageTitle>

<h1>Prerendered Counter 3</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override void OnInitialized()
    {
        if (!ApplicationState.TryTakeFromJson<int>(
            nameof(currentCount), out var restoredCount))
        {
            currentCount = Random.Shared.Next(100);
            Logger.LogInformation("currentCount set to {Count}", currentCount);
        }
        else
        {
            currentCount = restoredCount!;
            Logger.LogInformation("currentCount restored to {Count}", currentCount);
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistCount);
    }

    private Task PersistCount()
    {
        ApplicationState.PersistAsJson(nameof(currentCount), currentCount);

        return Task.CompletedTask;
    }

    private void IncrementCount() => currentCount++;

    void IDisposable.Dispose() => persistingSubscription.Dispose();
}

When the component executes, currentCount is only set once during prerendering. The value is restored when the component is rerendered. The following is example output.

Note

If the app adopts interactive routing and the page is reached via an internal enhanced navigation, prerendering doesn't occur. Therefore, you must perform a full page reload for the component to see the following output. For more information, see the Interactive routing and prerendering section.

info: BlazorSample.Components.Pages.PrerenderedCounter3[0]
currentCount set to 96
info: BlazorSample.Components.Pages.PrerenderedCounter3[0]
currentCount restored to 96

To preserve prerendered state, decide what state to persist using the PersistentComponentState service. PersistentComponentState.RegisterOnPersisting registers a callback to persist the component state during prerendering. The state is retrieved when the component renders interactively. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown.

The following counter component example persists counter state during prerendering and retrieves the state to initialize the component.

PrerenderedCounter2.razor:

@page "/prerendered-counter-2"
@implements IDisposable
@inject ILogger<PrerenderedCounter2> Logger
@inject PersistentComponentState ApplicationState

<PageTitle>Prerendered Counter 2</PageTitle>

<h1>Prerendered Counter 2</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override void OnInitialized()
    {
        if (!ApplicationState.TryTakeFromJson<int>(
            nameof(currentCount), out var restoredCount))
        {
            currentCount = Random.Shared.Next(100);
            Logger.LogInformation("currentCount set to {Count}", currentCount);
        }
        else
        {
            currentCount = restoredCount!;
            Logger.LogInformation("currentCount restored to {Count}", currentCount);
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistCount);
    }

    private Task PersistCount()
    {
        ApplicationState.PersistAsJson(nameof(currentCount), currentCount);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose() => persistingSubscription.Dispose();

    private void IncrementCount() => currentCount++;
}

When the component executes, currentCount is only set once during prerendering. The value is restored when the component is rerendered. The following is example output.

Note

If the app adopts interactive routing and the page is reached via an internal enhanced navigation, prerendering doesn't occur. Therefore, you must perform a full page reload for the component to see the following output. For more information, see the Interactive routing and prerendering section.

info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
currentCount set to 96
info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
currentCount restored to 96

By initializing components with the same state used during prerendering, any expensive initialization steps are only executed once. The rendered UI also matches the prerendered UI, so no flicker occurs in the browser.

The persisted prerendered state is transferred to the client, where it's used to restore the component state. During client-side rendering (CSR, InteractiveWebAssembly), the data is exposed to the browser and must not contain sensitive, private information. During interactive server-side rendering (interactive SSR, InteractiveServer), ASP.NET Core Data Protection ensures that the data is transferred securely. The InteractiveAuto render mode combines WebAssembly and Server interactivity, so it's necessary to consider data exposure to the browser, as in the CSR case.

Components embedded into pages and views (Razor Pages/MVC)

For components embedded into a page or view of a Razor Pages or MVC app, you must add the Persist Component State Tag Helper with the <persist-component-state /> HTML tag inside the closing </body> tag of the app's layout. This is only required for Razor Pages and MVC apps. For more information, see Persist Component State Tag Helper in ASP.NET Core.

Pages/Shared/_Layout.cshtml:

<body>
    ...

    <persist-component-state />
</body>

Interactive routing and prerendering

When the Routes component doesn't define a render mode, the app is using per-page/component interactivity and navigation. Using per-page/component navigation, internal† navigation is handled by enhanced routing after the app becomes interactive. †Internal in this context means that the URL destination of the navigation event is a Blazor endpoint inside the app.

The PersistentComponentState service only works on the initial page load and not across internal enhanced page navigation events.

If the app performs a full (non-enhanced) navigation to a page utilizing persistent component state, the persisted state is made available for the app to use when it becomes interactive.

If an interactive circuit has already been established and an enhanced navigation is performed to a page utilizing persistent component state, the state isn't made available in the existing circuit for the component to use. There's no prerendering for the internal page request, and the PersistentComponentState service isn't aware that an enhanced navigation has occurred. There's no mechanism to deliver state updates to components that are already running on an existing circuit. The reason for this is that Blazor only supports passing state from the server to the client at the time the runtime initializes, not after the runtime has started.

Additional work on the Blazor framework to address this scenario is under consideration for .NET 10 (November, 2025). For more information and community discussion of unsupported workarounds‡, see Support persistent component state across enhanced page navigations (dotnet/aspnetcore #51584). ‡Unsupported workarounds aren't sanctioned by Microsoft for use in Blazor apps. Use third-party packages, approaches, and code at your own risk.

Disabling enhanced navigation, which reduces performance but also avoids the problem of loading state with PersistentComponentState for internal page requests, is covered in ASP.NET Core Blazor routing and navigation.

Prerendering guidance

Prerendering guidance is organized in the Blazor documentation by subject matter. The following links cover all of the prerendering guidance throughout the documentation set by subject: