Tore Lervik

Optimizing without drawbacks

There are a lot of ways to optimize your website for faster delivery and smaller footprint. I'm looking at some optimization's in ASP.NET that doesn't cost in terms of being dynamic or locking down content because of caching issues.

General

Turn on gzip

Gzip is a feature that zip the content before delivering it to the client. With more and more mobile devices, gzip is getting more and more important. All though it has a slight overhead, the benefits are much greater than the cost.

Example: The JavaScript file for this website is 440KB. By only turning on gzip the size is now 160KB which is 36% of original. These sizes matter when your on 3G or other mobile networks.

Optimize images

There is an small but powerful tool available in the Extensions Manager in Visual Studio that is called Image Optimizer by Mads Kristensen. The tool allows you to right click on images in the solution explorer and generate optimized versions of the files. By running this tool on the images in my website project I got a total of 65% reduction in size without any loss in quality.



Microsoft.Web.Optimization

Microsoft has just released a Nuget packaged called ASP.NET Optimization - Bundling that adds support for easy minification and bundling of text-based (js, css, etc) files. The tool works by adding routes and rules to how and what it should bundle and minify.

After installing the package, add this to Application_Start in Global.asax

BundleTable.Bundles.EnableDefaultBundles();

By doing this you can now switch out the following

<link href="@Url.Content("~/Content/Reset.css")" rel="stylesheet" type="text/css" />
<link href="@Url.Content("~/Content/Base.css")" rel="stylesheet" type="text/css" />
<link href="@Url.Content("~/Content/FrontPage.css")" rel="stylesheet" type="text/css" />
<link href="@Url.Content("~/Content/ExtraStuff.css")" rel="stylesheet" type="text/css" />

with this

<link href="@BundleTable.Bundles.ResolveBundleUrl("~/Content/css")" rel="stylesheet" type="text/css" />

This will result in one file with minified css being sent to the client. The reason for this is that EnableDefaultBundles() we set up earlier add routes for /css and /js. The default rule look for all .css files in the directory and combines and minifies them before sending the result to the client. By doing the exact same thing just with ~/Scripts/js we would combine and minify all our .js files in the ~/Scripts directory.

BundleTable.Bundles.ResolveBundleUrl() creates an hash based on the combined files. This ensures that while combining and minified css you're not creating caching issues that often occur when doing these kinds of optimizations.

Custom routes and rules

While the default rules are great, we often have custom needs on how we want this to work. By default the rules will look for framework files (jquery, mootools, etc) first, then framework plugins and then all other files in an alphabetic order.

The following rule adds a route that /MyCss will result in a combined and minified file of ~/Content/MyCss.css and all files in the ~/Content/Styles directory.

var myCss = new Bundle("~/MyCss", typeof(CssMinify));
myCss.AddFile("~/Content/MyCss.css");
myCss.AddDirectory("~/Content/Styles", "*.css", false);
BundleTable.Bundles.Add(myCss);

Extending the default methods

You can also add your own methods of combining and minification. In this example I've stolen a CSSImageEmbedTransform method from Mads Kristensen talk on website optimization at this years Build conference. The method converts images that are used in css to be included as base64 strings instead of individual url's.

You can either inherit from CssMinify, JsMinify or IBundleTransform to create a new bundler. This example inherits from CssMinify and adds support for embedding images directly in the css.

public class CssWithImagesMinify : CssMinify
{
    private static readonly Regex url = new Regex(@"url\((([^\)]*)\?embed)\)", RegexOptions.Singleline);
    private const string format = "url(data:image/{0};base64,{1})";

    public override void Process(BundleResponse bundle)
    {
        HttpContext.Current.Response.Cache.SetLastModifiedFromFileDependencies();
        base.Process(bundle);
        string reference = HttpContext.Current.Request.Path.Replace("/css", "/");

        // When publishing the bundler can be called from "/" causing image reference to be invalid.
        if (reference == "/")
            reference = "/Content/";

        foreach (Match match in url.Matches(bundle.Content))
        {
            var file = new FileInfo(HostingEnvironment.MapPath(reference + match.Groups[2].Value));
            if (file.Exists)
            {
                string dataUri = GetDataUri(file);
                bundle.Content = bundle.Content.Replace(match.Value, dataUri);
                HttpContext.Current.Response.AddFileDependency(file.FullName);
            }
        }
    }

    private string GetDataUri(FileInfo file)
    {
        byte[] buffer = File.ReadAllBytes(file.FullName);
        string ext = file.Extension.Substring(1);
        return string.Format(format, ext, Convert.ToBase64String(buffer));
    }
}

All you need to do now is to add this in Global.asax to enable the new bundler on css files.

var css = new DynamicFolderBundle("css", typeof (CssWithImagesMinifier), "*.css");
BundleTable.Bundles.Add(css);
/* normal */
background-image: url(Images/myImage.png?embed);

/* with CssWithImagesMinifier */
background-image: url(data:image/png;base64,iVBORw0KGgoA...lFTkSuQmCC);



What's next?

html-minification

Css and JavaScript are both easy to minify and optimize as there are strict rules for how these formats operate. Html can be harder to minify, and it also need to be done in runtime which can be costly. Another major problem is that you need to ignore stuff inside pre and textarea tags. 

If you inspect the source of this website you will see that the html to some degree has been minified. I've written a small rule that intercepts output streams and run a couple of regex's on the content.

html = Regex.Replace(html, ">[ \r\n\t]+<", "> <", RegexOptions.Multiline | RegexOptions.Compiled);
html = Regex.Replace(html, " />", "/>", RegexOptions.Compiled);
html = Regex.Replace(html, @"<!--(?!\[)[\w\W]{1}((.|\n)+?)-->", "", RegexOptions.Compiled);

Depending on the content, the method above can give up to 20% reduction in size. But is it worth it considering the possible corruption of layout?


Summary

  • Gzip helps us reduce the size of text files
  • Reduce image size by ~65% by using an image optimizer
  • Bundle and minify both css, js and images by using the new ASP.NET opitimization package on nuget
    • 7 css files combined and minified as one - 70% reduction in size
      • Disk: 11KB
      • Minified (excluding the 9 images): 8KB
      • Gzip (excluding the 9 images): 2,9KB
    • 15 js files combined and minified as one - 81% reduction in size
      • Disk: 821KB
      • Minified: 440KB
      • Gzip: 160KB
    • 9 images included in css with our custom rule

Result

  • Reduced bytes being sent to the client by ~70% without using any caching.
  • Reduced 31 requests down to 2.