What is Prerendering?
First of all we have to clarify what does "prerendering" mean in Blazor. Why would one do this? And furthermore will this apply to Blazor Client as well as Blazor Server?
How would a normal initial request without prerendering look like?
- Our user hits our page. So our browser will go to the given address and load our index file.
- Our server now will serve different things depending if we use Client or Server
- Client: We are serving a
blazor.webassembly.js
which bootstraps the download of your application assemblies and .NET assemblies which are then compiled to WebAssembly and executed - Server: We are serving a
blazor.server.js
which creates a SignalR connection from your browser to the web server. The SignalR connection basically dispatches your inputs to the server and returns the new "HTML-Diff"
- Client: We are serving a
- The bootstraped version will render your component and display it to the User
What is the problem here? We have 2 problems. One for the client-side blazor and one for the server-side:
- Client-side: Before the user can interact with your website all the content has to be downloaded. That can take a while
- Client-side / Server-side: As we need JavaScript and SignalR or WebAssembly you will have a hard time optimizing your website for search engines. They often times don't execute JavaScript or will have problems with WebSockets / WebAssembly.
The solution: Prerendering Prerendering means just that we take the first request and pretend to be like a normal ASP.NET Core website. We just render everything and will return the content (static html) to the client (next to the other stuff described above). This is how it looks like:
Benefits
- For client-side blazor we show the initial site to the user without downloading the "big" .NET package. So the user has a way smoother experience
- For both: We enable SEO.
How to enable that?
Well that is super easy. You head to your _Host.cshtml
and add the render-mode
like the following
<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />
For client-side / WebAssembly
<component type="typeof(App)" render-mode="ServerPrerendered" />
For server side Blazor.
The Downside
Now the diagram is showing two "displays" / "renders", is this right? Yes. Have a look at the following component:
@page "/"
@inject IUserRepository userRepository
@foreach(var user in users)
{
<ShowUserInformation user="@item"></ShowUserInformation>
}
@code {
private List<User> users = new();
protected override async Task OnInitializedAsync()
{
Console.WriteLine("I was called");
users = await userRepository.GetAllUsers();
}
}
If you enable prerendering and you look on your Output console you will see the I was called
twice. The reason is simple. The first time OnInitializedAsync
is called happens on the server. So the server has to do all the work to create the static html website. After we are done and the content is sent to the user the second OnInitializedAsync
kicks in. As now WebAssembly or SignalR circuts starts they will do the same thing again. They didn't "know" that it was already rendered.
So if you do heavy work it would be done twice! Obviously a down site. Plus: Your user can see an obvious flicker as the page gets refreshed.
Is there something we can do?
.NET 6 to the rescue - meet persist-component-state
Now .NET6 brings a helper for exactly that case: persist-component-state
. Simplified it is a cached filled by the server and also sent to the client. So kind of a pre-filled cache.
Let's have a look at how does it work. The setup is a bit different for client-side and server-side Blazor.
Setup
Now let's have a look at the client-side first. Head to your Pages/_Host.cshtml
and add the following just before the end of your body
tag.
<body>
<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />
<persist-component-state />
</body>
So the simple <persist-component-state />
is totally enough.
Now how do we use it?
Let's have a look at our "new" Index.razor
@page "/"
@implements IDisposable
@inject PersistentComponentState applicationState
@inject IUserRepository userRepository
@foreach(var user in users)
{
<ShowUserInformation user="@item"></ShowUserInformation>
}
@code {
private const string cachingKey = "something_unique";
private List<User> users = new();
private PersistingComponentStateSubscription persistingSubscription;
protected override async Task OnInitializedAsync()
{
persistingSubscription = applicationState.RegisterOnPersisting(PersistData);
if (applicationState.TryTakeFromJson<List<User>>(cachingKey, out var restored))
{
users = restored;
}
else
{
users = await userRepository.GetAllUsers();
}
}
public void Dispose()
{
persistingSubscription.Dispose();
}
private Task PersistData()
{
applicationState.PersistAsJson(cachingKey, users);
return Task.CompletedTask;
}
}
Now let's unwrap that a bit:
@inject PersistentComponentState applicationState
this is the component which does the "caching" for us.- We enhanced our
OnInitializedAsync
withpersistingSubscription = applicationState.RegisterOnPersisting(PersistData);
. The idea is to create a subscription so that we add something to the cache it stays there on the client side as well. - If we close our webpage / navigate somewhere else, we have to cancel the subscription. This is done in the
Dispose
message. That is why we added@implements IDisposable
at the top - The following part is just: Do we have under the key some entry, if yes return it and populate it into
users
otherwise go to the repository.
if (!applicationState.TryTakeFromJson<List<User>>(cachingKey, out var restored))
{
users = restored;
}
else
{
users = await userRepository.GetAllUsers();
}
- Please be aware that is very important to have the same key for saving and restoring caching entries. If you every used
IMemoryCache
you are well aware.
Resources
If you want to know more about this topic have a look at the follwing: