Saturday, March 31, 2012

Configuring Bundles in MVC 4

We write a lot of JavaScript.

Thus the bundling, compression, and minification of JavaScript is important to the speed and performance of modern websites. This is why I love and have been a big advocate of tools like Combres, and also why I was so excited to hear that such features were (finally) coming built in to ASP.NET MVC 4.

Introducing MvcBundleConfig

MvcBundleConfig is a very simple minimalist project I wrote to add configuration and debugging features to MVC 4's bundling framework, and achieves all 6 six of the goals listed below. It requires only MVC4 to run, and you need only add one config file to your project, one line of code to your application start, and you are off and running.

NuGet Package: https://nuget.org/packages/MvcBundleConfig/

Source on GitHub: https://github.com/tdupont750/MvcBundleConfig

Before we get to the demonstration at the bottom, let's review the needs and wants of a good minification framework.

What I NEED in a minification tool:

  1. Compress resources into single files.
    • Multiple request take time and resources, neither of which are things that any of us have to spare. By compressing resources into single requests and can limit the overhead and load time on both our clients and our servers.
  2. Minify JavaScript and CSS content.
    • Minification removes as many unnecessary white spaces and characters as possible from your resource files, reducing file size by up to 80% on average. When then compounded with gzip, we can reduce the file size another 50%. This means that our web applications can be brought down to clients 90% faster.
  3. Make use of both client and server side caching.
    • Making 90% less requests is nice, and making those requests 90% smaller is even better, but only ever having to request or serve them once is the key to true performance. Unfortunately client and server caching can be a bit complex due to quirks of different technologies and browsers. A good minification framework should abstract these concerns away from us.
  4. Ability to turn off during debugging.
    • As fantastic as everything that we have listed about is for a production website, it is a nightmare for a development website. Debugging JavaScript is no less complicated or time consuming than debugging C#, and we need to be able to use a debuggers and other client side tools that are inhibited by minification. A good minification framework must expose a debugging mode that skips compression pipeline.

What I WANT in a minification tool:

  1. Simple and dynamic configuration.
    • I hate hardcoded configuration. It bloats my code, and it requires bin drops to deploy. Meanwhile I really like the ability to add simple configuration files to my site as often as I can. Config files are explicit, abstract, and can be updated at any time. Win.
  2. Take a few dependencies as possible.
    • I mentioned above that I like Combres and it has a reasonably sized code base, unfortunately the fact that it's NuGet package pulls down half a dozen additional dependencies makes it feel quite heavy. The fewer dependencies a framework takes the better.

MvcBundleConfig Examples

Bundles.config

<?xml version="1.0"?>
<bundleConfig ignoreIfDebug="true" ignoreIfLocal="true">
  <cssBundles>
    <add bundlePath="~/css/shared">
      <directories>
        <add directoryPath="~/content/" searchPattern="*.css" />
      </directories>
    </add>
  </cssBundles>
  <jsBundles>
    <add bundlePath="~/js/shared" minify="false">
      <files>
        <add filePath="~/scripts/jscript1.js" />
        <add filePath="~/scripts/jscript2.js" />
      </files>
    </add>
  </jsBundles>
</bundleConfig>

Global.asax.cs

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
    BundleTable.Bundles.RegisterTemplateBundles();
 
    // Only code required for MvcBundleConfig wire up
    BundleTable.Bundles.RegisterConfigurationBundles();
}

_Layout.cshtml

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <title>@ViewBag.Title</title>
 
        @* Call bundle helpers from MvcBundleConfig *@
        @Html.CssBundle("~/css/shared")
        @Html.JsBundle("~/js/shared")
    </head>
    <body>
        @RenderBody()
    </body>
</html>

In the Browser

NuGet Package: https://nuget.org/packages/MvcBundleConfig/

Source on GitHub: https://github.com/tdupont750/MvcBundleConfig

kick it on DotNetKicks.com

Enjoy,
Tom

27 comments:

  1. This is really cool indeed. I thought about the same after intitial investigatation of MVC4 B&M feature.

    What I don't like if .config file. This is so old-school :)..

    ReplyDelete
  2. Alex,

    If you don't want to use the config files I have added some simple collections that you can easily instantiate to wire up the configuration programmatically.

    Config files may be old school, but they are very effective.

    Tom

    ReplyDelete
  3. Hi Tom. You mention GZip in point 2, but I'm not sure how that would work in this solution. Would you mind expanding on that?

    ReplyDelete
  4. Qube,

    Modern browsers support resources that has been compressed. Just like zipping files to save space on disk, your server can gzip resources to make the smaller (and thus faster) when answering requests.

    The MVC bundling will take care of minification, and IIS can take care of the zipping: http://stackoverflow.com/questions/702124/enable-iis7-gzip

    Tom

    ReplyDelete
  5. This is good stuff! Thanks for your work on this. You should publish a Nuget package for it.

    Thanks!

    Nathan Bridgewater

    ReplyDelete
  6. A Nuget package would be a really good idea for this...

    ReplyDelete
  7. Question - do you have a way to force inclusion order? I have a file with a dependency on another file in a "library" directory. The dependent file is being loaded first, but it needs to load after. ex)

    {jsBundles}
    {add bundlePath="~/js/shared" minify="true"}
    {directories}
    {add directoryPath="~/Scripts/lib" searchPattern="*.js"}{/add}
    {/directories}
    {files}
    {add filePath="~/scripts/jquery.somethingdependent.js"}{/add}
    {/files}
    {/add}
    {/jsBundles}

    Or do I just have to split up my bundles and explicitly declare them in the correct order? Thanks.

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete
  9. Tom, great work! A couple of questions if I may ask:
    1) How do you enforce the bundling order?
    2) where in your code would you add so that the bundling url is prefixed with a CDN url as in --> bundleConfig ignoreIfDebug="true" ignoreIfLocal="true" cdnUrl="htt://mycdn.com"


    Any insight is appreciated.

    ReplyDelete
  10. NuGet Package: http://www.tomdupont.net/2012/05/mvcbundleconfig-nuget-package.html

    Jeremy,
    I do not believe that you can specify the inclusion order of a directory, but of course you can control the order by including the files.

    Gsogol,
    I am not sure that MVC4 has CDN support, I'll have to look that up.

    ReplyDelete
  11. Tom, sorry, let me clarify:

    my single file `jquery.somethingdependent.js` requires that jQuery be present. jQuery is one of the files in '~/Scripts/lib'. According to @ScottGu they should load alphabetically, then get automatically rearranged so that libraries like jQuery are loaded first (http://weblogs.asp.net/scottgu/archive/2011/11/27/new-bundling-and-minification-support-asp-net-4-5-series.aspx).

    However, in my case, the dependent file is being included first, then the 'lib' folder. I was actually asking two things - does your extension alter this behavior (unintentionally?), and/or how could we add some xml attribute to the config file to enforce loading order? Perhaps in `CreateBundleConfigs` or `AddBundleConfiguration`?

    ReplyDelete
    Replies
    1. Well, a little more digging shows that the "culprit" was `AddBundleConfiguration` in '\MvcBundleConfig\Extensions\BundleCollectionExtensions.cs'. It loaded each file from the bundle, *then* looped the directories to add them. This caused the dependent file to appear first in the bundle. I switched them, then recompiled your extension, and it fixed my problem.

      My question then becomes - what should the expected behavior be? Is your extension interfering with the default bundle inclusion logic mentioned by @ScottGu? Are you then supposed to reimplement the autosorting magic from the regular bundling?

      Regardless, your extension is awesome. I could probably avoid my issue by renaming my files with explicit alphabetical ordering ("js1-myfile.js, js2-anotherfile.js..."), and putting them in "presorted" folders. If that's the case, I'd suggest mentioning it in your documentation.

      Delete
    2. Thanks for peeking into the code and coming up with a solution. As far as expected behavior, I'll probably just start with your use case.

      You can expect an update sometime next week. :)

      Thanks Jeremy!

      Delete
  12. Hi, this is great work. Did you add in CDN Support ?

    ReplyDelete
  13. I like it, it's cool. Really helped to speed me up. Is there an xsd file I should be including somewhere though? The warnings get annoying.

    ReplyDelete
  14. Dewald,
    I have added CDN support in v2.1.0. :)

    Mike,
    I have not taken the time to create a XSD, put please feel free to make a pull request against the GitHub project.

    ReplyDelete
    Replies
    1. Can you please show how the bundle.config should look like with CDN support added?

      Delete
    2. This comment has been removed by the author.

      Delete
    3. The code makes it look like you add a "cdnPath" attribute to the "add bundlePath" node, but that doesn't appear to be working

      Delete
  15. This comment has been removed by the author.

    ReplyDelete
  16. This comment has been removed by the author.

    ReplyDelete
  17. With cdnPath we have to list each file individually. I went back from this solution to the solution specified here
    http://www.asp.net/mvc/tutorials/mvc-4/bundling-and-minification

    ReplyDelete

  18. Hi Tom,

    Can i manage which version to be use. I mean there are more than one JS file with version on it. But i want to choose one of them with out modify.

    Sorry for my bad english.

    Regard.

    ReplyDelete
  19. Tom,

    Getting an error trying to turn off minification.









    Error:

    Server Error in '/' Application.

    Object reference not set to an instance of an object.

    Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

    Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.

    Source Error:


    Line 59: @Html.JsBundle("~/js/types")
    Line 60: @Html.JsBundle("~/js/libraries")
    Line 61: @Html.JsBundle("~/js/shared") <--------- Offending line



    If I remove the minify it works correctly, if I set minify to "true" it also works correctly. Could you take a look?

    Thanks

    ReplyDelete
  20. And it stripped my config. Here it is again (minus the aligators)

    add bundlePath="~/js/shared" minify="false"
    directories
    add directoryPath="~/Scripts/Shared" searchPattern="*.js"
    /add
    /directories
    /add

    ReplyDelete
    Replies
    1. Should be fixed in v2.2.0.

      http://stackoverflow.com/questions/22050807/mvcbundleconfig-minify-false-issue/22060176#22060176

      Delete

Real Time Web Analytics