Using Bundler.NET for CSS and JavaScript Minification

Defining Bundles

You should define your bundles once when the application starts up. Actually it doesn’t matter where, as long as the bundle is defined before your page makes a request for it. In the Application_Start event is a good place.

Let's start with Javascript minification. Defining the bundle looks like this:

var js = new Bundle("~/js", typeof(JsMinify));
js.AddFile("~/Scripts/jquery-1.6.4.min.js");
js.AddFile("~/Scripts/jquery-ui-1.8.16.min.js");
js.AddFile("~/Scripts/global.js");
BundleTable.Add(js);

The constructor to create a new Bundle takes two parameters. The first is the virtual path your bundle will be requested by (we will change our 3 script tags in the layout page to 1 and point it to this path). The second parameter is the type of object that we will use to do the minification. There are several ways you can customize this but I'll get to that later.

After we've created our bundle we define which files will be included in it, then add it to the BundleTable. At this point the scripts are combined and minified.

If you include a file that doesn't exist it will be skipped. The order of adding files to the bundle matters. The resulting content will be combined in the same order you add files in.

This is one place where my implementation differs from the Microsoft one. They have this option but also another that will include an entire folder and it figure out the sorting. I thought that was rather liberal of them so I left that out.

Routing/Requesting Bundles

There is an HttpModule that handles the routing for bundles. Just include this in your config:

<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
    <add name="BundleModule" type="Bundler.BundleModule, Bundler" />
</modules>
</system.webServer>

I've also added it to the system.web section for running it locally or on IIS6 (say whaaa?)

<system.web>
    <httpModules>
        <add name="BundleModule" type="Bundler.BundleModule, Bundler" />
    </httpModules>
</system.web>

At this point we are ready for requests to ~/js and we can update our layout page. I can change the src attribute to the virtual path I defined, like so:

<script src="@Url.Content("~/js")" type="text/javascript"></script>

This will serve up the 3 JavaScript files we defined, combined into 1 and minified. Each response for a bundle is also gzipped and has the appropriate caching headers sent with it.

But won't the caching headers cause a problem when I change the content of one of my js files? Yes but...

To take advantage of caching and still keep users updated with our latest changes we can modify how we request the content.

At the top of the layout page include the Bundles namespace:

 

@using Bundler

Then change the script tag to

<script src="@BundleTable.ResolveUrl("~/js")" type="text/javascript"></script>

What this does is use a hash code generated from the content as an alias for the bundle. The output in my case looks like this:

<script src="/-5952548" type="text/javascript"></script>

and serves the same exact content. When you update one of your js files the hash code will change, thus the client will request a new file, and it will be cached until you change the js file again. *Minified content can be hard to debug, make sure to read the section on debugging.

This goes a little bit beyond the Microsoft implementation. Their solution was to output the alias as "js?v=5952548". One of the recommendations from PageSpeed is to not have query string parameters on static resources which is why I do not do this. They say some servers will not cache files with a query string.

Another example using CSS

CSS bundling is done the exact same way. I will just show the example since I've already explained it:

var css = new Bundle("~/css", typeof(CssMinify));
css.AddFile("~/Content/reset.css");
css.AddFile("~/Content/Site.css");
css.AddFile("~/Content/themes/base/jquery.ui.all.css");
BundleTable.Add(css);

And the layout page changes to:

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

Note that when minifying CSS content, URLs are updated to reflect the path to the bundle.

Minified Files Are Hard To Debug

Because minified content is hard to debug, when you are in debug mode (ie your config has

<compilation debug="true"...

) no minifcation will occur.

If you are using the BundleTable.ResolveUrl method you are still getting one file though, which can still be a pain to debug. For this case we have added another method for outputting HTML elements for bundles. To do this replace the ResolveUrl method with this code:

@BundleTable.RenderHtmlFor("~/css")

This will output a <link> element for every file in your bundle when in debug mode. When you are not in debug mode it will only output ONE <link> element to the minified version of all of the content. The same concept applies to Javascript bundles. This is the recommended way of referencing your bundles since it allows you to debug as you normally would and automatically take advantage of bundling when you are in production.

More Optimization with CSS

In the CSS file we can include some extra optimization if you have references to image files. Say for example we have this in our Site.css file:

body 
{
    background: #5c87b2 url(../images/bg.png) repeat-x;
}

The reference to this image can be combined with our css (also causing it to be gzipped and cached) by converting it to a base64 encoded image and including it in the content instead of having the browser make a separate request for it. To do this make the url reference point to the virtual path and include "?embed" after it, like this:

body 
{
    background: #5c87b2 url(~/images/bg.png?embed) repeat-x;
}

We also have to change the type of minifier to CssMinifyImageEmbed:

var css = new Bundle("~/css", typeof(CssMinifyImageEmbed));

CssMinifyImageEmbed inherits CssMinify and performs the image embedding on top of the bundling/minification. Now my request for the css will have that image embedded right in it. It looks something like:

background:#5c87b2 url(data:image/png;base64,iVBORw0KGgo...) repeat-x;

There is an exception to this when someone using IE7 (or lower) requests a CssMinifyImageEmbed bundle. Because IE7 cannot handle Base64 encoded images it sends a slightly different file with the virtual paths translated to their actual path. This makes it safe to use with all browsers.

Bundles on the Go

The best way to setup your bundles is in the Application_Start event but what if you have one-off bundles that are very small and are only included in one page? Good news - you can create bundles on the go. Just call this from your page:

BundleTable.CompositeUrl(Type type, params string[] virtualUrls)

Here is an example using Razor syntax:

<script src="@BundleTable.CompositeUrl(typeof(JsMinify), "~/Scripts/HomePage.js")" type="text/javascript"></script>

An virutal path for the bundle is not needed because the alias'ed URL will be returned from the method.

CSS Media Attribute and CSS3 Media Queries

When creating a package it is possible to specify the media type for internal and external CSS elements. There is a third parameter of the Bundle constructor which takes in a BundleOptions object. The BundleOptions object has a string property named Media which you can use to set the value which will be used for the "media" attribute when outputting a LINK element or STYLE element (depending on if you added a file or content to the bundle). In debug mode this will happen for every item in the bundle. Example:

var options = new BundleOptions { Media = "print" };
var printCss = new Bundle("~/printCss", typeof(CssMinify), options);
printCss.AddFile("~/Content/Print.css");
printCss.AddContent(".whitebg { background-color: #000; }");
BundleTable.Add(printCss);

Extending the Minifier

A good example of this is the CssMinifyImageEmbed class, so if you want an example check that out. What it does is inherit CssMinify and then override the Process() and SetHeaders() methods.

If you don't want to inherit from CssMinify or JsMinify you can implement IMinify and roll out something completely different.

If you don't like the caching headers included in the http response you can customize the request action. This fires on every request and is where the response (caching) headers are set.

BundleTable.Responses["~/js"].RequestAction = new Action(CustomRequestAction);

Or if you are extending the minifier with your own class, as mentioned above, just don't call base.SetHeaders() when overriding that method and do what it is you want instead.

Development Notes

The JsMinifier and CssMinifier use Microsoft's AjaxMin tool for minification. If the minifier has any errors it won't output the minified version and will include a list of errors in a comment at the top of the file.

If your config is set to debug mode, ie:

<compilation debug="true" targetFramework="4.0">

Then the bundling will happen on every request to make sure you always have the latest content, and some caching headers are not set on the response. When this is false (as it would be in production) the bundling only happens only once, when you add the bundle to the bundle table, and all of the caching headers are included.

Last edited Nov 27, 2012 at 5:31 AM by rushfrisby, version 11

Comments

No comments yet.