Next new feature that appeared in MVC 4 beta is bundling and minification. Idea is simple: modern web application contains of many static resources, especially css and javascript files. To load the page browser have to load each resource. Each resource is being requested by HTTP request. Browser have to make as many request as page refers, wait till all are completed and only after that render the page.
The problem is, each HTTP request takes time. One of the most critical Yahoo web applications performance recommendation says - Minimize HTTP request. As few HTTP requests you need to load application as better.
System.Web.Optimization
Bundling and minification is located in it’s own namespace System.Web.Optimization
and resides in assembly named Microsoft.Web.Optimization
, which is installed by default with new ASP.NET MVC4 application template as NuGet package. Since it’s still in beta, namespaces and class names are probably be changing, but let’s look on that we have now.
What is bundle?
Bundle is simply logical group of files that could be referenced by unique name and being loaded with one HTTP request. Suppose, we have a Layout.cshtml that might look something like that:
<meta charset="utf-8" />
<title>@ViewBag.Title</title>
<link rel="stylesheet" href="~/Content/jquery.mobile-1.0b3.min.css" />
<link rel="stylesheet" href="~/Content/reset.css" />
<link rel="stylesheet" href="~/Content/foundation.css" />
<link rel="stylesheet" href="~/Content/Fonts.css" />
<link rel="stylesheet" href="~/Content/Site.Layout.css" />
<link rel="stylesheet" href="~/Content/Site.Mobile.css" />
<script type="text/javascript" src="~/Scripts/jquery-1.6.4.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.mobile-1.0b3.js"></script>
<script type="text/javascript" src="~/Scripts/knockout-2.0.0.js"></script>
<script type="text/javascript" src="~/Scripts/underscore.js"></script>
<script type="text/javascript" src="~/Scripts/App/CacheInit.js"></script>
You can see, that’s a lot of css and javascript that will be loaded for page. With bundling we can rewrite it with very simple code
<meta charset="utf-8" />
<title>@ViewBag.Title</title>
<link rel="stylesheet" href="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Content/css")" />
<script type="text/javascript" src="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Scripts/js")"></script>
In this case "~/Content/css"
and "~/Scripts/js"
are no longer virtual paths, but bundle names.
The the browser would load this page, rendered HTML would look like:
<link href="/Content/css?v=q_sftc19r22licIM8-Ar58FwviyWry1JuYbA-iATm4M1" rel="stylesheet" type="text/css" />
<script src="/Scripts/js?v=_8kyYWxz-Je_VE0p3_w5nbcjAhq0Qj4vZiNxvYU_oBg1"></script>
Note, it would reference the bundle with version. The version is a kind of hash taken on all files in bundle content. This enables browser caching, if content of bundle is not change browser will take it from cache, which is much faster. In case of changes, new version token is generated, so browser would be forced to reload bundle.
Bundle registration
In global.asax
file you will see new line:
BundleTable.Bundles.RegisterTemplateBundles();
It’s only one small line of code that enables bundling and minification framework.
RegisterTemplateBundles() vs. EnableDefaultBundles()
It looks fine so far, so I tried to add new javascript file into Scripts folder. Unfortunately, my application did not work. I think this is first very contra-intuitive fact of bundling framework. Googling a bit I found quick solution, change RegisterTemplateBundles() to EnableDefaultBundles().. I tried that and it really works.
Since the System.Web.Optimization
has not been opened sourced yet (in case you haven’t heard - ASP.NET web stack is open source, even with accepting pull request) I had to go to JustDecompile to understand why is that.
So, RegisterTemplateBundles() is looks like that,
public void RegisterTemplateBundles()
{
bool flag;
bool flag2;
bool flag3;
bool flag4;
bool flag5;
bool flag6;
bool flag7;
Bundle bundle1 = new Bundle("~/Scripts/js", new JsMinify());
bool flag8 = false.AddDirectory("~/Scripts", "jquery-*", flag, flag8);
bool flag9 = false.AddDirectory("~/Scripts", "jquery.mobile*", flag2, flag9);
bool flag10 = false.AddDirectory("~/Scripts", "jquery-ui*", flag3, flag10);
bool flag11 = false.AddDirectory("~/Scripts", "jquery.unobtrusive*", flag4, flag11);
bool flag12 = false.AddDirectory("~/Scripts", "jquery.validate*", flag5, flag12);
bool flag13 = false.AddFile("~/Scripts/MicrosoftAjax.js", flag13);
bool flag14 = false.AddFile("~/Scripts/MicrosoftMvc.js", flag14);
bool flag15 = false.AddDirectory("~/Scripts", "modernizr*", flag6, flag15);
bool flag16 = false.AddFile("~/Scripts/AjaxLogin.js", flag16);
this.Add(bundle1);
Bundle bundle2 = new Bundle("~/Content/css", new CssMinify());
bool flag17 = false.AddFile("~/Content/site.css", flag17);
bool flag18 = false.AddDirectory("~/Content/", "jquery.mobile*", flag7, flag18);
this.Add(bundle2);
Bundle bundle3 = new Bundle("~/Content/themes/base/css", new CssMinify());
bool flag19 = false.AddFile("~/Content/themes/base/jquery.ui.core.css", flag19);
bool flag20 = false.AddFile("~/Content/themes/base/jquery.ui.resizable.css", flag20);
bool flag21 = false.AddFile("~/Content/themes/base/jquery.ui.selectable.css", flag21);
bool flag22 = false.AddFile("~/Content/themes/base/jquery.ui.accordion.css", flag22);
bool flag23 = false.AddFile("~/Content/themes/base/jquery.ui.autocomplete.css", flag23);
bool flag24 = false.AddFile("~/Content/themes/base/jquery.ui.button.css", flag24);
bool flag25 = false.AddFile("~/Content/themes/base/jquery.ui.dialog.css", flag25);
bool flag26 = false.AddFile("~/Content/themes/base/jquery.ui.slider.css", flag26);
bool flag27 = false.AddFile("~/Content/themes/base/jquery.ui.tabs.css", flag27);
bool flag28 = false.AddFile("~/Content/themes/base/jquery.ui.datepicker.css", flag28);
bool flag29 = false.AddFile("~/Content/themes/base/jquery.ui.progressbar.css", flag29);
bool flag30 = false.AddFile("~/Content/themes/base/jquery.ui.theme.css", flag30);
this.Add(bundle3);
}
As you can see it adds all recourses that came just in template, including jQuery Mobile and jQuery UI.
In the same time, EnableDefaultBundles()
public void EnableDefaultBundles()
{
this.Add(new DynamicFolderBundle("js", JsMinify.Instance, "*.js"));
this.Add(new DynamicFolderBundle("css", CssMinify.Instance, "*.css"));
}
So, what it does - it matches all javascript and css files in project, minify them and create to bundles out of it.
As you can see EnableDefaultBundles()
is also uses Minification policy for bundle content.
How add my own custom bundle?
As simple as create new Bundle
object, put files inside and then add it to Bundles collection.
var bundle = new Bundle("~/Scripts/libs", new JsMinify());
bundle.AddFile("~/Scripts/knockout-2.0.0.js");
BundleTable.Bundles.Add(bundle);
But there two issues here. First of all, you don’t always want to minify. Second, adding bundles with many files will make global.aspx.cs looks messy. I really liked approached proposed by Scott K. Allen in his Yet Another Bundling Approach for MVC 4 blog post. He does it in more object-oriented way including smart code to decide, should the bundle be minified or not. All you need is just take code he created and after you are able to make bundles like that.
public class JsLibsBundle : JsBundle
{
public JsLibsBundle() : base("~/js/libs")
{
AddFiles(
"~/Scripts/jquery-1.6.4-vsdoc.js",
"~/Scripts/jquery-1.6.4.js",
"~/Scripts/jquery.mobile-1.0b3.js",
"~/Scripts/knockout-2.0.0.js",
"~/Scripts/modernizr-2.0.6-development-only.js",
"~/Scripts/underscore.js"
);
}
}
public class CssAppBundle : CssBundle
{
public CssAppBundle() : base("~/css/mobile")
{
AddFiles(
"~/Content/jquery.mobile-1.0b3.css",
"~/Content/Site.Mobile.css"
);
}
}
And put them to registered those classes in global.asax.cs
BundleTable.Bundles.Add(new JsLibsBundle());
BundleTable.Bundles.Add(new CssAppBundle());
Ok, how all that stuff works?
Now let’s do a really brief look under the hood. Again, JustDecompile is our friend here.
It was interesting to me to find out new cool way of registration modules in ASP.NET. No longer entry points in web.config or other magic. You can have a special class PreApplicationStartCode
that would be called then assembly is loaded, but before application is stated.
public static class PreApplicationStartCode
{
private static bool _startWasCalled;
public static void Start();
}
Here System.Web.Optimization
register it’s own module, called BundleModule
.
public static void Start()
{
if (PreApplicationStartCode._startWasCalled)
{
return;
}
PreApplicationStartCode._startWasCalled = 1;
DynamicModuleUtility.RegisterModule(typeof(BundleModule));
}
Then the module is intialized, it subscribes for event called HttpApplication.PostResolveRequestCache and inside the callback it would register the BundleHandler
.
private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
{
HttpApplication httpApplication = (HttpApplication)sender;
if (BundleTable.Bundles.Count > 0)
{
BundleHandler.RemapHandlerForBundleRequests(httpApplication);
}
}
The handler is now responsible for dirty job. Through the RequestBundle
class it finally comes to ProcessRequest
method.
internal void ProcessRequest(BundleContext context)
{
context.EnableInstrumentation = Bundle.GetInstrumentationMode(context.HttpContext);
BundleResponse bundleResponse = this.GetBundleResponse(context);
Bundle.SetHeaders(bundleResponse, context);
context.HttpContext.Response.Write(bundleResponse.Content);
}
So, the GetBundleResponse
is there magic is happens.
private BundleResponse GetBundleResponse(BundleContext context)
{
BundleResponse bundleResponse = Bundle.CacheLookup(context);
if (bundleResponse == null || context.EnableInstrumentation)
{
bundleResponse = this.GenerateBundleResponse(context);
if (!context.EnableInstrumentation)
{
Bundle.UpdateCache(context, bundleResponse);
}
}
return bundleResponse;
}
What’s important here is that it uses cache. So, it makes bundling rather efficient. In case if there are no cached result, it will run the GenerateBundleResponse
to generate actual response:
public virtual BundleResponse GenerateBundleResponse(BundleContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
IEnumerable<FileInfo> fileInfos = this.EnumerateFiles(context);
fileInfos = context.BundleCollection.IgnoreList.FilterIgnoredFiles(fileInfos);
fileInfos = this.Orderer.OrderFiles(context, fileInfos);
fileInfos = this.ReplaceFileExtensions(context, fileInfos);
string str = this.Builder.BuildBundleContent(this, context, fileInfos);
BundleResponse bundleResponse = new BundleResponse(str, fileInfos);
this.Transform.Process(context, bundleResponse);
return bundleResponse;
}
Yet another interesting finding here is this.Orderer
. Sometime ago I did smalltalk on Kiev ALT.NET meeting about new stuff in VS2011 and being asked a question, how is possible to order the files. I didn’t know answer then. It is actually possible to setup the order of files inside bundle, in case you have cross-file dependencies:
public interface IBundleOrderer
{
IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files);
}
That’s it, the generated response than just being written to context.HttpContext.Response
.
Conclusions
Bundling and minification is something that I personally wanted so much, to appear in new MVC framework. Now it’s there, it works.. but I would not say I’m 100% happy. There are several things that makes it a little difficult to use, as for me. I did want to place it in this post, but appeared to big.. So, I’ll probably do a separate one on this matter.