ASP.NET MVC has had server-side bundling and minification for a couple versions now. You can use this to reduce HTTP requests from the client browser to the web server. This optimization is a must when you get lots of small JavaScript files, which is what most large web applications have these days.
Here's the default BundleConfig.cs in an MVC4 default project:
using System.Web;
using System.Web.Optimization;
namespace BundlingSample
{
public class BundleConfig
{
// For more information on Bundling, visit http://go.microsoft.com/fwlink/?LinkId=254725
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include(
"~/Scripts/jquery-ui-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.unobtrusive*",
"~/Scripts/jquery.validate*"));
// Use the development version of Modernizr to develop with and learn from. Then, when you're
// ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
"~/Scripts/modernizr-*"));
bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));
bundles.Add(new StyleBundle("~/Content/themes/base/css").Include(
"~/Content/themes/base/jquery.ui.core.css",
"~/Content/themes/base/jquery.ui.resizable.css",
"~/Content/themes/base/jquery.ui.selectable.css",
"~/Content/themes/base/jquery.ui.accordion.css",
"~/Content/themes/base/jquery.ui.autocomplete.css",
"~/Content/themes/base/jquery.ui.button.css",
"~/Content/themes/base/jquery.ui.dialog.css",
"~/Content/themes/base/jquery.ui.slider.css",
"~/Content/themes/base/jquery.ui.tabs.css",
"~/Content/themes/base/jquery.ui.datepicker.css",
"~/Content/themes/base/jquery.ui.progressbar.css",
"~/Content/themes/base/jquery.ui.theme.css"));
}
}
}
This approach works well for 3rd party libraries (jQuery, jQuery validation, knockout, etc.), and it has a cache-busting hash in the URL that keeps you from caching an old version of the bundled and minified script in your browser.
Here you can see jquery and modernizr have a query string after the file name with the unique key:
So we've got bundling of scripts, minification, and cache-busting. What's not to love?
Where do I put my application scripts?
The default MVC project gives you a \Scripts folder. You could put your application scripts there. NuGet would like you to leave 3rd party scripts there, and it's the convention, but I prefer to move scripts I didn't write for the current app into a \Scripts\lib folder, and scripts for the app go in a \Scripts\app folder.
You'll have some moving around to do when you update a JavaScript via NuGet to get it in your preferred folder, but it's a little easier to find the stuff you'll really be working on for your app. You'll also need to update your BundleConfig.cs to point to the new \Scripts\lib folder.
So how to we bundle these app scripts? You could bundle them all in one file. Everything in that folder gets put in a "~/bundles/myApp" bundle. The problem is, you'll likely end up with lots of sequencing problems in the scripts and see "undefined" errors. This could work if you only have a small amount of JavaScript in your app, but it's not recommended.
You could also create a Razor section in the _Layout.cshtml and call it "scripts", then each view can list the scripts it needs to work in that Razor section. The default MVC4 _Layout.cshtml file already has this Razor section set up for you. This approach works, but now we're not bundling or minifying the JavaScript, and we don't have any cache busting going on with our app scripts.
So I have to debug minified files? Yuck.
The other drawback of using the built-in ASP.NET optimizations for bundling and minification is debugging minified files. You've probably dealt with this when your JavaScript throws an exception and you're in the middle of a jQuery file. JQuery almost certainly doesn't have a bug, but you sent it some values it wasn't expecting. Have fun sorting that out.
![[image_thumb_11.png](https://cdn.volaresoftware.com/images/posts/2013/3/image_thumb_11.png)
IE10 and Chrome have JavaScript pretty print tools that can make the minified script a little easier to read than one long line, but the variable names will all still be a, b, c, and whatever the minifier hadn't already used.
Using map files is a better way to go, and in Chrome, you turn this on in dev tools and click the gear at the bottom right, then check "Enable source maps".
Now each minified file can have a map file that points to the original, unminified source code. The built-in ASP.NET optimizations don't work with map files yet.
Web Essentials bundling to the rescue
If you are a Visual Studio web developer and you don't have Web Essentials, go get it now. You'll love it. It's a Visual Studio extension created my Mads Kristensen, who works for Microsoft for the Web Platform and Tools group.
One of the features is bundling and minification. You select the files you want to bundle, right click, then choose Web Essentials > Create JavaScript bundle file.
Name your bundle, and several files will be created. The first is the *.bundle file:
<?xml version="1.0" encoding="utf-8"?>
<bundle minify="true" runOnBuild="true">
<!--The order of the <file> elements determines the order of them when bundled.-->
<file>/Scripts/app/script1.js</file>
<file>/Scripts/app/script2.js</file>
</bundle>
This file lists each script in the bundle and has flags for minify (default is "true") and runOnBuild (default is "true"). You may need to rearrange the order of the bundle files if there are functions defined in one file and called in another.
Any change you make to script1.js or script2.js, the *.bundle file, or building your app, will regenerate the files underneath the *.bundle file:
These three files are 1) the combined bundle without minification, 2) the bundle with minification, and 3) the map file for debugging the original script. The first is a giant concatenation of the two files and isn't that interesting.
The minified file looks like this:
function add(n,t){return n+t}function subtract(n,t){return n-t}function showMessage(n){alert(n)}
//@ sourceMappingURL=myAppBundle.min.js.map
And the map file looks like this:
{
"version":3,
"file":"myAppBundle.min.js",
"lineCount":1,
"mappings":"AAAAA,SAASA,GAAG,CAACC,CAAC,CAAEC,CAAJ,CAAO,CACf,OAAOD,CAAE,CAAEC,CADI,CAInBC,SAASA,QAAQ,CAACF,CAAC,CAAEC,CAAJ,CAAO,CACpB,OAAOD,CAAE,CAAEC,CADS,CCJxBE,SAASA,WAAW,CAACC,CAAD,CAAM,CACtBC,KAAK,CAACD,CAAD,CADiB",
"sources":["/Scripts/app/script1.js","/Scripts/app/script2.js"],
"names":["add","x","y","subtract","showMessage","msg","alert"]
}
So the minified file gives the browser instructions on how to get to the map file, which gives the browser instructions on how to render in unminified, made-for-humans format.
Using the bundles
I create a bundle of app scripts for each view (usually I name it after the view, like register.bundle, login.bundle, etc.) and use the Razor "scripts" section in the _Layout.cshtml file to place those scripts at the bottom of the html, just before the closing
tag. The script reference points to the minified file.@section scripts {
<script src="~/Scripts/app/myAppBundle.min.js"></script>
}
In Chrome, the end result is this for the minified file:
And this for the mapped file:
This human-readable script can have breakpoints, etc. It looks like the original file, but the browser has not created new HTTP requests to pull down the originals. It's map file magic.
Setting up view-specific bundles seems lighter weight to me than always editing the BundleConfig.cs file, plus you get the mapping files. Now we just need to solve for the cache busting we lost with this approach.
Cache busting
Ever send your product owner to the CI web server to check out a new feature, only to have them tell you they got JavaScript errors on that page? Did you tell them to type CTRL-F5 to force a refresh of the page? If so, you have a caching problem where the browser is caching the file even though it has changed.
Mads has a blog post on cache busting that works well. It's based on getting the last changed date of the file and appending that date in ticks to the path of the request for the script or css. His solution involves having URL rewriting working on IIS.
In my case, I didn't want to mess with URL rewriting, so I went with the somewhat less optimal but simpler solution of using a query string instead. Here's my Razor helper:
using System.IO;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
namespace BundlingSample.Extensions
{
public static class StaticFile
{
public static string Version(string rootRelativePath)
{
if (HttpRuntime.Cache[rootRelativePath] == null)
{
var absolutePath = HostingEnvironment.MapPath(rootRelativePath);
var lastChangedDateTime = File.GetLastWriteTime(absolutePath);
if (rootRelativePath.StartsWith("~"))
{
rootRelativePath = rootRelativePath.Substring(1);
}
var versionedUrl = rootRelativePath + "?v=" + lastChangedDateTime.Ticks;
HttpRuntime.Cache.Insert(rootRelativePath, versionedUrl, new CacheDependency(absolutePath));
}
return HttpRuntime.Cache[rootRelativePath] as string;
}
}
}
and how it's used in the Razor view:
@section scripts {
<script src="@StaticFile.Version("~/Scripts/app/myAppBundle.min.js")"></script>
}
and the resulting file name in the browser:
The query string pattern matches the built-in ASP.NET optimization pattern. I say this is sub-optimal because some page speed tools will fuss at you for using query strings. Google PageSpeed says, "Enabling public caching in the HTTP headers for static resources allows the browser to download resources from a nearby proxy server rather than from a remote origin server." I'm not too worried about that, but you'll want to use Mads' URL rewrite approach if you want a better PageSpeed score.
So there you have it. Bundling, minification, debugging, and cache busting when you need it!