Inlining CSS and JavaScript Bundles with ASP.NET MVC

When you want to load a CSS file within an HTML page, you typically use a <link> tag within the <head> section of the page. When the browser parses the HTML response and encounters the <link> tag, it makes another HTTP request to fetch the external CSS file that has been referenced.

The advantage of this approach is that the browser can cache the CSS file. During subsequent page visits, the stylesheet doesn't have to be downloaded again. Instead it can be served directly from the browser cache, which is blazingly fast. Also, loading the stylesheet from a cache saves data volume on mobile devices using cellular data.

However, the weakness of external CSS files lies in the first page request. If the browser doesn't yet have a copy of the stylesheet in its cache, it has to go out and fetch the CSS file. During that time, it won't continue rendering the page because CSS files are render-blocking resources. Since the <link> is placed within the <head> section, the user basically stares at a blank screen.

Obviously, the longer it takes to request an external CSS file, the longer the rendering process is blocked. Network latency can be high, especially on mobile devices. Because the browser can only know which CSS files to download once the HTTP response for the HTML page is back, it has to make the HTTP requests sequentially (rather than in parallel) and therefore incurs the latency costs twice:

Sequential HTTP requests for an HTML document and a CSS file

Inlining Stylesheets into HTML

For this reason, it makes sense for small CSS files to be inlined into the HTML document using <style> tags. No additional stylesheet resources have to be fetched that way, which in turn reduces render block times. After all, the fastest HTTP request is the one not made.

Note that you should only inline small CSS files. For large stylesheets (e.g. the full Bootstrap framework), the benefits of caching outweigh the benefits of faster rendering. It doesn't make sense to ship an additional (uncacheable) 500 KB of inline styles every time a page is requested just to make the first page load slightly faster.

So let's look at how we can inline CSS files into HTML using the System.Web.Optimization framework and its bundles. (You're concatenating and minifying your scripts and stylesheets already, right? If not, make sure to read this introduction to bundling and minification before you go on.)

Of course, we don't want to manually add the CSS to our Razor views. It's tedious, messy, and doesn't work well with Sass or other preprocessor languages. It would be much nicer if we could just inline the contents of a StyleBundle that we've already created.

Inlining Style Bundles (CSS)

Since we want to let the System.Web.Optimization framework do the heavy lifting of bundling and minifying our stylesheets, we somehow need to get hold of the generated CSS. Let's create a method that returns the contents of a bundle with a given virtual path:

private static string LoadBundleContent(HttpContextBase httpContext, string bundleVirtualPath)
{
    var bundleContext = new BundleContext(httpContext, BundleTable.Bundles, bundleVirtualPath);
    var bundle = BundleTable.Bundles.Single(b => b.Path == bundleVirtualPath);
    var bundleResponse = bundle.GenerateBundleResponse(bundleContext);

    return bundleResponse.Content;
}

It creates a BundleContext from the current HttpContext, finds the bundle with the given virtual path, and finally returns the generated response as a string. If no bundle with the given virtual path could be found, the Single extension method throws an exception, which is a good thing — no silent failures here!

Now, let's create an extension method for HtmlHelper that we can call to generate the appropriate <style> tags:

public static class HtmlHelperExtensions
{
    public static IHtmlString InlineStyles(this HtmlHelper htmlHelper, string bundleVirtualPath)
    {
        string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath);
        string htmlTag = string.Format("<style>{0}</style>", bundleContent);

        return new HtmlString(htmlTag);
    }

    private static string LoadBundleContent(HttpContextBase httpContext, string bundleVirtualPath)
    {
        // ...
    }
}

Note that we're returning an IHtmlString here to indicate that we don't want the return value to be HTML-encoded later. That said, the above code is all we need to inline our CSS files. We can now use our new extension method to inline the content of all files in an exemplary CSS bundle into the HTML response:

<head>
    <!-- ... -->
    @Html.InlineStyles("~/Client/styles/main-bundle.css")
</head>

Instead of <link> tags, you'll now see a <style> tag containing inline CSS. Sweet!

<head>
    <!-- ... -->
    <style>.some-css { /* The CSS generated by the bundle */ }</style>
</head>

Inlining Script Bundles (JavaScript)

This entire blog post has been about CSS files and inline stylesheets so far, but doesn't the same also apply to JavaScript files? Yes, absolutely.

Loading external JavaScript files via <script src="..."> has the same pros and cons as loading CSS files through <link> tags. It also makes sense to inline some small JavaScript files that contain code which should run as soon as possible.

Similar to the CSS approach, we should be able to call the following method:

@Html.InlineScripts("~/Client/scripts/main-bundle.js")

Here's how our two extension methods InlineScripts and InlineStyles can look like. Now that we have two of them, I've extracted the InlineBundle method that renders either a <script> tag or a <style> tag, depending on the bundle type:

using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;

public static class HtmlHelperExtensions
{
    public static IHtmlString InlineScripts(this HtmlHelper htmlHelper, string bundleVirtualPath)
    {
        return htmlHelper.InlineBundle(bundleVirtualPath, htmlTagName: "script");
    }

    public static IHtmlString InlineStyles(this HtmlHelper htmlHelper, string bundleVirtualPath)
    {
        return htmlHelper.InlineBundle(bundleVirtualPath, htmlTagName: "style");
    }

    private static IHtmlString InlineBundle(this HtmlHelper htmlHelper, string bundleVirtualPath, string htmlTagName)
    {
        string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath);
        string htmlTag = string.Format("<{0}>{1}</{0}>", htmlTagName, bundleContent);

        return new HtmlString(htmlTag);
    }

    private static string LoadBundleContent(HttpContextBase httpContext, string bundleVirtualPath)
    {
        var bundleContext = new BundleContext(httpContext, BundleTable.Bundles, bundleVirtualPath);
        var bundle = BundleTable.Bundles.Single(b => b.Path == bundleVirtualPath);
        var bundleResponse = bundle.GenerateBundleResponse(bundleContext);

        return bundleResponse.Content;
    }
}

You'll also find the above code in this Gist. And there we go, here's our inline JavaScript:

<body>
    <!-- ... -->
    <script>(function() { /* The generated JavaScript */ })();</script>
</body>

Use the coupon code LAUNCHDAY for $10 off!

Learn React

8 Comments

The Dude

Thanks! I had been looking at resources like Critical CSS and wasn't sure how to prevent render blocking resources on this .NET MVC project. This was really helpful.

Anders Lybecker

Thanks - great post.

Do you know of a tool that can extract the required CSS/JavaScript required for a single page from the site wide CSS/JavaScript? It would be really helpful.

Stiven

Great post!

Thanks

Matt

but surely you would want to add a call to the actual script so it was cached for the next use, but then you would have to see if it was the 1st request again and it the script had changed.

rgshare

This will throw System.InvalidOperationException, maybe we should RegisterBundles first? I am using MVC5

Carl-Erik Kopseng

@Anders Lybecker: Regarding your question on "a tool that can extract the required CSS" me and Jonas Ohlson created Penthouse that does exactly this.

I developed the command line interface to it, but I did not have time to maintain it, so these days you use it as a Node module. Running Node scripts can easily be integrated as part of your build (google it), so this should do what you require.

Dan

Thanks for this post. I set myself the (seemingly) humble task of making a brand new MVC project with stock bootstrap, pass the Google "Page Speed Insights" test 100% for both Mobile & Web. Deferring Javascript was easy enough but CSS was a real headache.

Tried async, tried rel=import to a static html page, tried Google's own javascript hack (which worked on mobile but not desktop, strangely)...

This is the only thing that got me to 100% on Page Insights. Nice one :)

Fabio Milheiro

Very useful. Got it working super quickly! Thanks