Mark Oliver's World

Posted: 27/05/2021

Speeding Up The Initial Load

My site is a static WASM one, there is no server host involved.
Therefore in order to display the dynamic content of the site, all the "code" must be downloaded to the browser, and then run within the browser to generate the content.

This is a downside to Blazor WASM, and the clever peeps at Microsoft have been trying to improve this. The current plan they have is to pre-render the content. However this requires a Server host at some point to generate the dynamic content and store a static version of it to then display to the user.

But can we pre-render without a hosted site at all?

Reading these gave me an idea:

Problem

I want my users to be able to start reading the blog page much faster than they currently do. This is primarily aimed at direct links to posts. At the moment, they get a "Loading" screen for quite a while. This is a holding page while the Blazor JS kicks off and downloads dotnet.wasm and all the dotnet binaries needed to actually display anything.

So pre-rendering seems sensible, as it will display something to the user much faster. But without a host, this loading page is still an issue right?

So why not change the Loading page to be the first page of my website. It gives the user something to read for a few seconds while the Blazor code is downloading, and then it will switch to the Blog post.

This means replacing the Index.html with a version of my home page!

This is very akin to the old loading screen games when computer games used to take an age to load.

A discovery

In playing with the ideas above, I discovered something!

In a non-hosted Blazor WASM setup (like mine), I have found that having a HTML file of the same name in wwwroot folder, means it will get served to the user first.
If we ensure that the <script defer src="_framework/blazor.webassembly.js"></script> tag is in that file, then Blazor will kick in after the page is loaded, and then display the dynamic version when it has all the bits to do it.

e.g.
If we hit this Url

https://blog.markoliver.website/Setting-Up-A-Sitemap
and in the wwwroot folder have a html file called "Setting-Up-A-Sitemap.html" (Note we have to have the file with a .HTML extension with the exactly the same name as the Uri path!) then we see the static file first, before we see the dynamic one:

An animation of the pre rendering in action

So with this information, we have an idea...

Possible Solution

If we use Andrew Locks idea to visit every page and save the rendered output to a static html file, ensure that the blazor.webassembly.js script is on every page, then the pre-rendered output will be shown BEFORE Blazor has finished loading all the .NET dlls (about 20MB worth) in the background.

This requires the pre-rendered files to be saved into the wwwroot dir. The pre-rendered files MUST have the blazor.webassembly.js script included too, otherwise it will never switch to the dynamic view!

Design

I need to generate the pre-rendered views of the pages.
Andrew lock does it by generating them using a hosted site.
But what about using bUnit to generate the content and inserting it into a templated html file. No hosted site needed at all!

First, I need to get a list of Urls to generate. Luckily this is held in my Index data store.
So I can read that list, and then using bUnit render the BlogPage.razor component and take the HTML out, and insert it into a templated HTML file with the blazor.webassembly.js script in it, and save the file in the correct place.

Simples right?

Lets find out...

Note - I have not changed my Blazor WASM site at all to accommodate this. All the tutorials on the web talk about removing index.html and things like that, but they are for a hosted site. Mine is not. All I am doing is adding html pages into the wwwroot folder!

Implementation

Setup the bUnit text context

This needs external JavaScript calls disabled (e.g. Twitter) as we don't care about the rendering from this. Not this does not affect the Blazor generation.

            Bunit.TestContext ctx = new Bunit.TestContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; //Ignore any JS calls
            
          

Then I needed to register the service dependencies that my BlogPage Blazor component uses.

            IBlogPostAquirer mockedblogPostAquirer = CreateMockForBlogPostAquirer(); ctx.Services.AddScoped<HttpClient>(); ctx.Services.AddScoped<BlogPostReader>(); ctx.Services.AddScoped<IBlogPostIndexReader, BlogPostIndexReader>(); ctx.Services.AddScoped<IBlogPostPopulater, BlogPostPopulater>(); ctx.Services.AddScoped<IBlogPostAquirer>( s => { return mockedblogPostAquirer; } ); ctx.Services.AddScoped<MarkDownFileUriBuilder>(); ctx.Services.AddScoped<MarkOliverBlog.Searching.Searcher>(); ctx.Services.AddScoped<IWebAssemblyHostEnvironment>( s => { return Mock.Create<IWebAssemblyHostEnvironment>(); } );
            
          

I needed to Mock out IBlogPostAquirer, instead of trying a HttpClient call to an external url for a MarkDown file, it can just read the local one in the source directory:

            var blogPostAquirer = Mock.Create<IBlogPostAquirer>(); Mock.Arrange( () => blogPostAquirer.GetPost( Arg.AnyString ) ).Returns( ( string fileName ) => {     return Task.FromResult( File.ReadAllText( $"{basePath}/Posts/{fileName.Replace( " ", "" )}.md" ) ); } ); return blogPostAquirer;
            
          

All other dependencies are the real ones, as I want the generation to be as close to reality as possible.

Call bUnit

Tell bUnit to render the BlogPage component:

            Bunit.IRenderedComponent<BlogPage> systemUnderTest = ctx.RenderComponent<BlogPage>( parameters => parameters.Add( p => p.Title, blogName ) ); systemUnderTest.WaitForAssertion( () => systemUnderTest.Find( ".tagHeader" ), TimeSpan.FromSeconds( 10 ) );
            
          

Create the static file

I read the current Index.html file, as it contains the majority of the layout we need and it forms the basis of all Blazor generated pages.
I then swap out the app div for the generated content from bUnit. This uses AngleSharp which was included as part of bUnit anyway.
I also remove the #blazor-error-ui div as it's not needed in the static data.

            string currentTemplate = File.ReadAllText( basePath + "index.html" ); var config = Configuration.Default; var context = BrowsingContext.New( config ); var parser = context.GetService<IHtmlParser>();  var document = parser.ParseDocument( currentTemplate ); var body = document.QuerySelector( "body" ); body.RemoveChild( document.QuerySelector( "#blazor-error-ui" ) );  var app = document.QuerySelector( "#app" ); app.InnerHtml = generatedContent;
            
          

Finally I just write the file out to disk in the wwwroot directory.

Next steps

  • I need to pre-render other pages, which is the same process but with a different set of components.
  • Inline all the CSS that is needed in the pre-rendered page, so no unnecessary external files are downloaded: This looks good: https://github.com/milkshakesoftware/PreMailer.Net
  • Defer the javascript code to run as late as possible, ideally after render, so we can get the HTML shown to the user.
  • Try to make the change from Static page to Dynamic page as seamless as possible for the user.
  • Improve the look of the pre-rendered page.

Conclusion

This works great in the sense that the page is shown as close to immediate as we can get (so far). However on slow connections, it still takes time to download the dotnet binaries. We are relying on the user to be reading for the time it takes to download those files in the background.
Hopefully they will be downloaded before the next page is requested, but at least the user has gotten the initial content as soon as possible.
The only way we can reduce that is by ensuring the smallest amount of content and external files are needed, which is an ideal for every website!

This approach will work for all future posts. It runs as a Unit Test which means that the static content will also get auto deployed via my GitHub Action to the Azure static web app.

I love this approach, I only had to write a few lines of code, and not change my Blog at all. It uses everything I already have, and will work without change as my Blog grows.

The best part of all is that the initial time to see content is so much smaller (its hard to measure accurately, but Its about 3 seconds compared with 21 seconds when running the "Fast 3G" throttling option in Chrome on a new Incognito window directly to a blog post).

So that is about 7 times faster for new site visitors!

Also, according to Google Chromes Lighthouse dev tool. The Time to Interactive is down from 4.2s to 1.9s

Lets see it in action then with a side by side comparison:
An animation of the pre rendering in action as a side by side comparison

Matt Smith as Doctor Who Saying Who Da Man


Thanks for reading this post.

If you want to reach out, catch me on Twitter!

I am always open to mentoring people, so get in touch.