While prerendering is now the default for server-side Blazor applications, I only recently discovered (as in the last 48 hours via Daniel Roth's work) that client-side Blazor applications can take advantage of this as well. In this post, I'm going to show you how you can setup your client-side Blazor application for prerendering.

The example project for this post can be found on GitHub.

What is prerendering?

Prerendering is a process where all the elements of a web page are compiled on the server and static HTML is served to the client. This technique is used to help SPAs (Single Page Applications) improve their SEO (Search Engine Optimisation). Another benefit is that sites appear to load much faster.

What this means for a Blazor application is that the requested page will be built on the server and compiled to static HTML. This static HTML will include the blazor.webassembly.js file which is present in the standard client-side Blazor template. When the client receives this static HTML it will be processed and rendered as normal.

When the blazor.webassembly.js file is executed the mono runtime will be downloaded along with the application dlls and your application will be run. At this point all of the static prerendered elements will be replaced with interactive components and the application will become interactive.

Now, this may sound like a lot has to happen before your application becomes usable. But this all happens in a very short space of time and is imperceivable to most end users.

The Prerendering Trade-off

Before we look at how to enable prerendering I want to point out that there are some trade-offs with using it.

You will no longer be able to deploy your Blazor application as static files. As we'll see in a second, prerendering requires a razor page, and that means a .NET runtime is required. While this is probably not a big issue, I do want to make sure you're aware of it.

The other trade-off is that you must manage any JavaScript calls to account for prerendering. If you attempt to perform JavaScript interop in the OnInit or OnInitAsync method of a component which is being prerendered then you will get an exception. When using prerendering all JavaScript interop calls should be moved to the OnAfterRenderAsync life-cycle method. This method will only be called once the page is fully rendered.

Enabling Prerendering

We're going to start with a stand alone Blazor application and go though the steps to enable prerendering. I've created a new stand alone Blazor application using the .NET CLI. You can also use the template in Visual Studio.

dotnet new blazorwasm -o BlazorPrerendering.Client

If you are looking to enable prerendering on a client-side Blazor app that is already using the "Hosted" template. Then you can just add in the bits of configuration as we go along.

Adding a Host Project

The first thing we are going to do is add a new empty ASP.NET Core Web App. Again, I'm using the .NET CLI but you can use the templates in Visual Studio if you prefer.

dotnet new web -o BlazorPrerendering.Server

Our solution should now look like this.

Then we need to add a project reference from the server project to the client project as well as a couple of NuGet packages. So the easiest thing to do is edit the csproj file directly.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <LangVersion>7.3</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Blazor.Server" Version="3.1.0-preview1.19508.20" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0-preview1.19508.20" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorPrerendering.Client\BlazorPrerendering.Client.csproj" />
  </ItemGroup>

</Project>

Once you're done, your project file should look like the code above.

Configuring The Host

Now our projects are setup, we are going to make some changes to the server projects Startup.cs.

First, add the following code to the ConfigureServices method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddNewtonsoftJson();
    services.AddResponseCompression(opts =>
    {
        opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
            new[] { "application/octet-stream" });
    });
    
    services.AddScoped<HttpClient>(s =>
    {
        var uriHelper = s.GetRequiredService<IUriHelper>();
        return new HttpClient
        {
            BaseAddress = new Uri(uriHelper.GetBaseUri())
        };
    });
}

We'll also need to add some using statements.

using System;
using System.Net.Http;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.ResponseCompression;

Then replace the code in the Configure method with the code below.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseResponseCompression();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBlazorDebugging();
    }
    
    app.UseClientSideBlazorFiles<Client.Startup>();

    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
        endpoints.MapFallbackToPage("/_Host");
    });
}

Finally, we need to create a folder called Pages in the root of the server project and create a file called _Host.cshtml with the following code.

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Prerendering client-side Blazor</title>
    <base href="~/" />
    <environment include="Development">
        <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    </environment>
    <environment exclude="Development">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
              asp-fallback-href="css/bootstrap/bootstrap.min.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
              crossorigin="anonymous"
              integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" />
    </environment>
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))</app>

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

Testing Prerendering

We should now be able to start up the server project and launch the application. Once the application has loaded, the easiest way to test prerendering is to disable JavaScript in your browser. Then reload the page, if the page loads then prerendering is working.

To achieve this in Chrome or Edgeium, open the dev tools and press ctrl+shift+p or cmd+shift+p, depending on your OS. The start typing JavaScript, you should see the option appear to disable JavaScript.

You should still be able to navigate around the app but you will find components will not be interactive. Go ahead and enable JavaScript again (just repeat the steps to disable but now the option will be to Enable JavaScript) and refresh the page, you should now have an interactive application once more.

Summary

Prerendering is a really useful tool to have available and it's great that we can now use it with both server-side and client-side Blazor. In this post, I've shown how you can enable prerendering of your client-side Blazor applications by use of a hosted server project.