Inlining a stylesheet using a TagHelper in ASP.NET Core
When you have a very tiny stylesheet, it may be more performant to inline it directly in the page. Indeed, it will avoid one network call, and the layout may be blocked until the browser gets the response of this call. If the stylesheet is very small you may not care about caching the file on the client. For instance, on this website, the main stylesheet is 2kB large, and the full page with the inlined stylesheet is about 11kB. It's so small that I've chosen to inline the stylesheet. Of course, I don't want to paste the content of the minified CSS file in the html page by hand. The page would be unreadable, and if you format your document, it will add indentation or line break. So, it's not maintainable. Instead, you want it to be automatic. Using a tag helper it could look like:
<inline-style href="css/site.min.css" />
Let's see how to create this tag helper!
First, let's create the structure of the Tag Helper. A tag helper is just a class that inherits from TagHelper
public class InlineStyleTagHelper : TagHelper
{
[HtmlAttributeName("href")]
public string Href { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
// TODO
}
}
To use the TagHelper, you need to declare it in the _ViewImports.cshtml
. There is another way to include the Tag Helper as explain in the documentation, but this is the most common.
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyWebApp [Replace MyWebApp with the name of the assembly that contains the TagHelper]
Now, we'll read the file on the disk and copy it to the output. To get the full file path from the relative path, you need to know the root path. You can use IHostingEnvironment.WebRootFileProvider
to get it. Tag Helpers support dependency injection, so you can inject IHostingEnvironment
in the constructor. Here's what it looks like:
public class InlineStyleTagHelper : TagHelper
{
public InlineStyleTagHelper(IHostingEnvironment hostingEnvironment)
{
HostingEnvironment = hostingEnvironment;
}
[HtmlAttributeName("href")]
public string Href { get; set; }
private IHostingEnvironment HostingEnvironment { get; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var path = Href;
IFileProvider fileProvider = HostingEnvironment.WebRootFileProvider;
IFileInfo file = fileProvider.GetFileInfo(path);
if (file == null)
return;
string fileContent = await ReadFileContent(file);
if (fileContent == null)
{
output.SuppressOutput();
return;
}
// Generate the output
output.TagName = "style"; // Change the name of the tag from inline-style to style
output.Attributes.RemoveAll("href"); // href attribute is not needed anymore
output.Content.AppendHtml(fileContent);
}
private static async Task<string> ReadFileContent(IFileInfo file)
{
using (var stream = file.CreateReadStream())
using (var textReader = new StreamReader(stream))
{
return await textReader.ReadToEndAsync();
}
}
}
#Caching the file in memory
That works great! However, in terms of performance, it is not optimal. You need to read the disk for each request. A solution is to cache the content of the file in memory. However, you have to take care of flushing the cache when the file changes on the disk. ASP.NET Core comes with everything you need to cache values and handle the expiration:
- You can cache a value using the
IMemoryCache
- You can handle the expiration using an
IChangeToken
Hopefully, the IFileProvider
allows us to get this kind of token using the Watch
method, so the code is very simple to write:
public class InlineStyleTagHelper : TagHelper
{
public InlineStyleTagHelper(IHostingEnvironment hostingEnvironment, IMemoryCache cache)
{
HostingEnvironment = hostingEnvironment;
Cache = cache;
}
[HtmlAttributeName("href")]
public string Href { get; set; }
private IHostingEnvironment HostingEnvironment { get; }
private IMemoryCache Cache { get; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var path = Href;
// Get the value from the cache, or compute the value and add it to the cache
var fileContent = await Cache.GetOrCreateAsync("InlineStyleTagHelper-" + path, async entry =>
{
IFileProvider fileProvider = HostingEnvironment.WebRootFileProvider;
IChangeToken changeToken = fileProvider.Watch(path);
entry.SetPriority(CacheItemPriority.NeverRemove);
entry.AddExpirationToken(changeToken);
IFileInfo file = fileProvider.GetFileInfo(path);
if (file == null || !file.Exists)
return null;
return await ReadFileContent(file);
});
if (fileContent == null)
{
output.SuppressOutput();
return;
}
output.TagName = "style";
output.Attributes.RemoveAll("href");
output.Content.AppendHtml(fileContent);
}
}
At the end, you should see a lot of CSS directly in the page:
Demo of a stylesheet inlined by the TagHelper
This tag helper works as expected! You can now do similar things for your JavaScript files or the small images that can be embedded using base64.
Do you have a question or a suggestion about this post? Contact me!