Tuesday 5 March 2019

Improving an old application - MVC Bundler

I'm maintaining an MVC application that was built in 2015. It works, but it doesn't work great, so today I'm looking at assets.

Thankfully at least it uses MVC Bundler for bundling and minification. This is a good tool for an app of this era.

These are the bundled scripts (only JS) according to Chrome:




As you can see there are a few bundles, as well as some manually loaded JavaScript files. Interesting. The benefit of bundling is that you don't use as many browser connections to the server to load your assets, thereby allowing the page to load more at the same time - and potentially do it faster.

Let's have a look at the source code (_Layout.cshtml):

@Styles.Render("~/Content/bootstrap")
@Styles.Render("~/Content/site")
@Scripts.Render("~/bundles/respond")
@Scripts.Render("~/bundles/modernizr")
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/jqueryval")
@Scripts.Render("~/bundles/bootstrap")

<script src="~/Scripts/ckeditor/ckeditor.js"></script>
<!-- kept having problem with those files in a bundle: some file were not loaded and order is important -->
@if (HttpContext.Current.IsDebuggingEnabled)
{
    <script src="~/Scripts/knockout-3.1.0.debug.js"></script>
}
else
{
    <script src="~/Scripts/knockout-3.1.0.js"></script>
}
<script src="~/Scripts/knockout-bootstrap.min.js"></script>
<script src="~/Scripts/knockout.mapping-latest.js"></script>
<script src="~/Scripts/knockout.validation.js"></script>
This is odd - note the comment about bundler problems, there's some manual minification-file selection, and a lot of bundles here - respond, modernizr, jquery, jqueryval, bootstrap and ckeditor could all be combined.

So I thought I'd at least bundle all the knockout assets The layout file now looks like this:
@Styles.Render("~/Content/bootstrap")
@Styles.Render("~/Content/site")
@Scripts.Render("~/bundles/respond")
@Scripts.Render("~/bundles/modernizr")
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/jqueryval")
@Scripts.Render("~/bundles/bootstrap")

@Scripts.Render("~/bundles/ckEditor")
@Scripts.Render("~/bundles/knockout")

And the bundle config for the knockout bundle. Note I've used {version} so that MVC Bundler can pick the correct version and .min file when debugging:
bundles.Add(new ScriptBundle("~/bundles/knockout").Include(
   "~/Scripts/knockout-{version}.js",
   "~/Scripts/knockout-bootstrap.js",
   "~/Scripts/knockout.mapping-latest.js",
   "~/Scripts/knockout.validation.js"
));

Now to reload the page, and... Oops, a JavaScript error "Uncaught ReferenceError: guid is not defined". This had me stumped for a while - I had a look at bundle ordering as per the original comment (see this SO post for some good tips) - but that didn't help. So I turned to the generated HTML to see what it looked like (this is just the last two bundles):
<script src="/Scripts/ckeditor/ckeditor.js"></script>
<script src="/Scripts/knockout-3.1.0.debug.js"></script>
<script src="/Scripts/knockout.mapping-latest.debug.js"></script>
<script src="/Scripts/knockout.validation.js"></script>

You can see the ckeditor bundle, then three files from the knockout bundle. Why aren't there four?

Let's have a look at the Scripts directory:

All assets are there in some form, but notice that knockout-bootstrap only has a min.js file. Bundler tries to pick the correct JavaScript based on the DEBUG environment setting and potentially the directive
BundleTable.EnableOptimizations = true

Perhaps the lack of a non-minified knockout-bootstrap file is causing problems? I downloaded the old version of knockout-bootstrap unminified and placed it in the Scripts folder. Now the generated HTML looks like this:
<script src="/Scripts/ckeditor/ckeditor.js"></script>
<script src="/Scripts/knockout-3.1.0.debug.js"></script>
<script src="/Scripts/knockout-bootstrap.js"></script>
<script src="/Scripts/knockout.mapping-latest.debug.js"></script>
<script src="/Scripts/knockout.validation.js"></script>

Success! The page loads. However, let's look at some of those other bundles. We could trim about 10 bundles into one, as they're always loaded from the MVC layout page. We can merge all the scripts and styles into two:
@Styles.Render("~/Content/site")
@Scripts.Render("~/bundles/scripts")

And the resulting number of loaded JS assets drops substantially:

The page load time is now a lot snappier, from 600+ms to just 70ms for the JS assets. (Rounding as I haven't don't thousands of samples to get an average!)

Finally, let's look at knockout and knockout.mapping to make sure that MVC Bundler knows how to handle .debug files. Normally bundler expects my.js (unminified) and my.min.js (minified), but in this case we have my.debug.js (unminified) and my.js (minified).

Rather than look for the code inside the big bundle, I put an alert('knockout minified'); statement in knockout.js and tested it in bundled/release mode. It works! Bundler is smart enough to pickup .min files in release mode or .debug files in debug mode.

This exercise taught me two things:
  1. Don't assume that the work has already been done. This application is live and has been used for about 4 years.
  2. Don't trust comments! I like well commented code when required, but in this case the comment was pointing me to the wrong reason.
Next I may look at reducing the minified 1.3MB down to something more respectable, but this will do for now!

No comments:

 
Copyright 2009 Another Blog. Powered by Blogger Blogger Templates create by Deluxe Templates. WP by Masterplan