Loading stylesheets asynchronously using a TagHelper in ASP.NET Core

 
 
  • Gérald Barré

In a previous post, I talked about inlining a CSS file in the HTML page. In terms of performance, inlining critical CSS is important. However, the rest of the page's styles do not need to be downloaded immediately. For script elements, you can add the async attribute to instruct the browser to load the resource asynchronously. For stylesheets, there is no equivalent. You have to write a custom JavaScript snippet to simulate the same behavior. You must also handle the case where scripts are disabled.

Here's what the html looks like to load stylesheets asynchronously:

HTML
<noscript id="render-onload">
    <link rel="stylesheet" href="/css/prism.min.css" />
</noscript>
<script>
    var renderOnLoad = function() {
        var e = document.getElementById('render-onload');
        var n = document.createElement('div');
        n.innerHTML = e.textContent;
        document.body.appendChild(n);
        e.parentElement.removeChild(e)
    };

    if(window.requestAnimationFrame) {
        window.requestAnimationFrame(function(){window.setTimeout(renderOnLoad, 0)})
    } else {
        window.addEventListener('load', renderOnLoad);
    }
</script>

The idea is to replace the noscript element with a div once the page has loaded, so the code is interpreted and the browser begins loading the assets. This prevents the stylesheet from blocking the initial rendering of the page.

With a TagHelper, you can replace this HTML code with a single tag:

HTML
<render-on-load>
    <link rel="stylesheet" href="~/css/prism.min.css" asp-append-version="true" />
</render-on-load>

This is far more readable and reusable across pages or projects. Tag helpers are very useful for extracting reusable behaviors!

If you are not familiar with Tag Helpers, you should read some of my previous posts:

The TagHelper works by replacing the tag name with noscript and appending the JavaScript snippet after the element. Unlike previous tag helpers, this one needs to read the element's inner content. You can do that using output.GetChildContentAsync(), which processes the Razor content so that other tag helpers are also evaluated. This is why asp-append-version="true" works correctly on the inner link element.

C#
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

[HtmlTargetElement("render-on-load")]
public class RenderOnPageLoadTagHelper : TagHelper
{
    [HtmlAttributeName("id")]
    public string Id { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var content = await output.GetChildContentAsync();
        output.TagName = "noscript";

        var id = string.IsNullOrEmpty(Id) ? "render-onload" : Id;
        output.Attributes.Add("id", id);
        output.PostElement.AppendHtml("<script>var renderOnLoad=function(){var e=document.getElementById('" + id + "'),n=document.createElement('div');n.innerHTML=e.textContent,document.body.appendChild(n),e.parentElement.removeChild(e)},r=window.requestAnimationFrame;r?r(function(){window.setTimeout(renderOnLoad,0)}):window.addEventListener('load',renderOnLoad);</script>");
    }
}

To use the TagHelper, declare it in _ViewImports.cshtml. There are other ways to include Tag Helpers as explained in the documentation, but this is the most common.

HTML
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyWebApp [Replace MyWebApp with the name of the assembly that contains the TagHelper]

Here's how to use it:

HTML
<render-on-load>
    <link rel="stylesheet" href="~/css/prism.min.css" asp-append-version="true" />
</render-on-load>

<render-on-load id="images">
    <img src="~/images/banner1.png" asp-append-version="true" />
    <img src="~/images/banner2.png" asp-append-version="true" />
</render-on-load>

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?