Marius Schulz
Marius Schulz
Front End Engineer

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) 500KB 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>