I have been using Reactive UI with Blazor Server for years, and I’ve often noticed double data loadings in applications I’ve developed. This issue can significantly impact application performance by unnecessarily increasing network usage, slowing down page loads, and creating a poor user experience. I fixed this by turning off pre-rendering, but I’ve never fully understood why it happened.
Recently, I experimented with PersistentComponentState
, hoping to transfer state between the pre-rendering phase and the final rendering phase. My goal was to resolve the double loading issue while still benefiting from pre-rendering. However, I discovered that pre-rendering—even in .NET 9 (SDK 9.0.101)—behaves inconsistently. There also seems to be an unnecessary “page-loading” phase that wastes CPU and memory resources without achieving anything meaningful. I reported this issue on the .NET GitHub repository: Issue #59569.
To better understand the problem, let’s review how Blazor pre-rendering works in .NET 9.
Pre-render Effects in Blazor Server Interaction
Pre-rendering is enabled by default for Blazor Server. Whenever a user navigates to a page marked with @rendermode InteractiveServer
, Blazor renders the page twice:
Static Rendering: The first render is for static content, which benefits search engine crawling and robots.
Interactive Rendering: The second render initializes the in-memory state of the Blazor circuit.
During static rendering, Blazor triggers lifecycle methods such as OnInitialized
, OnParametersSet
, and SetParametersAsync
, but not OnAfterRender
. In the interactive phase, all lifecycle methods are triggered. Developers typically load data in OnInitialized
, which leads to the double loading issue. To mitigate this, Microsoft introduced PersistentComponentState
to share state between these phases.
Understanding Static Rendering
Static rendering behaves like an MVC framework where it has access to HttpContext
as a CascadingParameter
. It is stateless, meaning Blazor creates temporary states, including DI contexts, for static rendering and discards them afterward. PersistentComponentState
is the only mechanism to carry serializable state into the interactive phase.
Static rendering occurs not only on the page’s initial load but also during navigation via NavigationManager
(e.g., clicking a <NavLink>
), which might be understandable when occasional data updates on the screen are necessary. However, PersistentComponentState
is only effective during the initial load. Static rendering during navigation doesn’t utilize PersistentComponentState
, leading to the creation and discarding of states without updating the screen. This behavior appears more like a defect than an intentional design choice.
Example Code
Here’s an example demonstrating the behavior:
@page "/"
@rendermode InteractiveServer
@implements IDisposable
@inject PersistentComponentState State
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
<p>Count = @myCount</p>
Welcome to your new app.
@code {
PersistingComponentStateSubscription? ps;
static int count;
[CascadingParameter] HttpContext? HttpContext { get; set; }
int myCount = Interlocked.Increment(ref count);
protected override void OnInitialized() {
ps = State.RegisterOnPersisting(Persist);
var existed = State.TryTakeFromJson<int>("data", out var c);
Console.WriteLine($"{myCount}. Created with persisted={existed}, c={c}, service={State.GetHashCode()}, and context={HttpContext}");
}
Task Persist() {
State.PersistAsJson("data", myCount);
return Task.CompletedTask;
}
protected override void OnAfterRender(bool firstRender) {
Console.WriteLine($"{myCount}. OnAfterRender {firstRender}");
}
public void Dispose() {
ps?.Dispose();
}
}
This modified example from the Blazor skeleton project assigns a counting number whenever OnInitialized
is called and prints out state details. Below is the output when loading the page, navigating away, and returning to the page:
1. Created with persisted=False, c=0, service=16484805, and context=Microsoft.AspNetCore.Http.DefaultHttpContext
2. Created with persisted=True, c=1, service=23091838, and context=
2. OnAfterRender True
3. Created with persisted=False, c=0, service=23749772, and context=Microsoft.AspNetCore.Http.DefaultHttpContext
4. Created with persisted=False, c=0, service=23091838, and context=
4. OnAfterRender True
Explanation of the Output
Step 1: The first static render during the initial load. HttpContext
is accessible.
Step 2: The interactive render after static rendering. PersistentComponentState
retrieves the persisted state.
Step 3: Static rendering occurs during navigation, creating and discarding states without utilizing them. This takes place after navigating to another page and returning.
Step 4: The interactive render after navigating back, but PersistentComponentState
is no longer effective.
Another unexpected behavior occurs when navigating to the current page, such as <NavLink>
with href="/"
. In this case, only the static rendering takes place, but the screen remains unchanged, leading to a confusing and seemingly redundant action.
To clarify, observe the service numbers in the output panel. This number is a unique hash that functions like an object ID. Steps 1 and 3 have entirely different numbers, while steps 2 and 4 share the same number. This happens because the PersistentComponentState
service is registered with a "scoped" lifetime in the DI context, meaning the scoped states in the interactive session are shared. The service is created when the Blazor circuit is initialized and is disposed of when the circuit ends. Since steps 2 and 4 are in interactive mode, they use the same service instance, resulting in the same number. However, steps 1 and 3 are static renders, and their instances are temporary, preventing them from sharing states.
It would be acceptable if PersistentComponentState worked consistently in step 4. However, even with that functionality, the peculiar behavior of pre-rendering when navigating to the current page remains problematic, particularly for applications that experience heavy usage.
My Two Cents
The current pre-rendering in interactive server mode is not yet suitable for production due to its tendency to perform unintentional and wasteful loads. To make it production-ready, improvements are necessary, such as consistent handling of PersistentComponentState
during navigation and eliminating redundant static rendering phases when revisiting the same page. Until these issues are resolved, it’s advisable to disable pre-rendering using @rendermode @(new InteractiveServerRenderMode(prerender: false))
.
Top comments (0)