Tuesday, December 14, 2010

Minimize JavaScript and CSS with MSBuild: Part 1

This is going to be a two part posts about how to use MSBuild to automate JavaScript and CSS minimization.  Part 1 is mostly introduction so if you want to jump straight to how it’s done go to part 2.

For the project I am working on, I am trying to use best practice by minimize JavaScript and CSS files to reduce download time.  However, it’s hard to develop JavaScripts with no whitespaces and meaningless variable names.  What I decided to do is to have the build process automatically invoke a tool to minimize CSS and JavaScript for me during compilation.

There are many tools out there that would minimize the files for you.  I checked out .Net implementation of YUI CompressorGoogle Closure Library, and Microsoft Ajax Minifier.  I need a tool that integrate with MSBuild, so that eliminates Google’s tool, which is too bad since it produces the best result from what I can gather online.  While it is true that I can use the command line task to execute Google’s tool, I would also need to setup the environment for Java and make sure it’s available on build server.  The YUI Compressor combines all the CSS files and JavaScript files into a single css/js file, which is not the behavior I want for my project.  I want to keep page specific files separate so they don’t get downloaded until user visit that page.  In another word, if the user visit AboutUs page he shouldn’t have to download the bits for CheckBrowser page.  In the end I decided to use Microsoft Ajax Minifier.

For the rest of this post I will describe the sample project and how it’s setup.  The sample program tests a subset of browser’s html5 functionality support.  The sample program uses Asp.Net MVC2, jQuery, Modernizr, and SilkSprite.  Here are the results in Chrome 7 and IE 8:

IE 8

Chrome 7

Here is the list of the files in the project, the folders of interests are uncompressed folder and content folder.  Take note of scripts and styles folder in both of the directory.  The only difference is the files under content has ‘min’ before the extension to indicate they are minimized version of the file.  The ‘uncompressed’ folder is where your working version of the files would be.  So let’s setup the folder structure first.

Create the ‘uncompressed’ folder and add ‘scripts’ and ‘styles’ directory to the folder.  Next add/move the desired files from ‘Content’ and ‘Scripts’ folders to their appropriate location under ‘uncompressed’ fold.  MVC template automatically adds the ‘Content’ folder to your project, you can rename it to ‘content’ if you are anal like me, if not just leave it in place.  Then add ‘images’, ‘scripts’, and ‘styles’ folder to ‘content’.

Next we need to grab the files for tools we will be using and place them in the project.  Download Modernizr JavaScript from http://www.modernizr.com/, and SilkSprite from http://www.ajaxbestiary.com/Labs/SilkSprite/.  I will assume you know how to create controllers and views since that’s not the scope of this article but if not you can check out a few tutorials online.  I de-minimized Modernizr script for testing purpose, you can leave it minimized and simple make sure it doesn’t have ‘min’ in the file name.  I also change the SilkSprite CSS to point to the location of actual image file.

Finally we would need to do is set the build action for every file under ‘uncompressed’ to None.  To do this you click on the file and bring up the property window(F4), the first item in the property window should be ‘Build Action’, change its value to None and do it for all the files.

That’s it for the background explanation and setup of the project.  In the next post we will open up the MSBuild file and modify the internal of the project file to add instruction for minimize JavaScript and CSS files during compilation.

Download the sample project here.

Tuesday, December 7, 2010

Minimize JavaScript and CSS with MSBuild: Part 2

This is part 2 of two part posts discussing how to use MSBuild to automate JavaScript and CSS minimization.  Part 1 containing background information on what we are trying to accomplish and basic project setup.  This post will dive into the steps required to setup the minimization process.

With the project structure setup the way we wanted, the next thing we are going to do is to make the project automatically pick up all file under ‘content’ folder and treat them all as content.  With the sample project open right click on the project file “Html5” and select “Unload Project”, then right click on the ghosted project icon and select “Edit Html5.csproj”.  Search for /Project/ItemGroup/Content xml element and find all the Content element with Includes attribute referencing files in the ‘content’ folder.  Delete all of them, then add this element in it’s place, make sure it’s a child of <ItemGroup> element.

<Content Include="content\**\*.*" />

Before we proceed to the next step, make sure you have Microsoft Ajax Minifier installed.  Check for “C:\Program Files (x86)\MSBuild\Microsoft\MicrosoftAjax\AjaxMin.tasks”.  Back to the Html5.csproj file, find the <Import> element and add a sibling node to the file.

<Import Project="$(MSBuildExtensionsPath32)\Microsoft\MicrosoftAjax\AjaxMin.tasks" />

Find the Target element Named “BeforeBuild” (<Target Name=”BeforeBuild”…), it might be commented out, uncomment it and remove all child elements from it.  If BeforeBuild target doesn’t exist, add it to the file.  We are doing the customization in BeforeBuild because BeforeBuild target is referenced by Build target, and the Build target is called every time before debugging or deployment.  This behavior will make sure the minimized files are updated before we launch the program for testing or package it for deployment.

The content inside BeforeBuild target is pretty simple it does two things: first it delete previous minimized file from content directory, then it calls two other targets to perform the actual work of minimizing and copy content files.  Here is the xml snippets for BeforeBuild target:

<Target Name="BeforeBuild">
    <JsToDelete Include="content\scripts\**\*.*" />
    <CssToDelete Include="content\styles\**\*.*" />
  <Delete Files="@(JsToDelete)" />
  <Delete Files="@(CssToDelete)" />
  <CallTarget Targets="MinimizeWebContent;CopyWebContent" />

Take note of CallTarget task, it calls two targets we referenced earlier.  We will look at the CopyWebContent target first.  We use CopyWebContent to copy file as-is for debugging and testing purpose.  We need to make sure this target is only executed when our configuration is set to Debug.  For that we will set the condition on the target, Condition=" '$(Configuration)' == 'Debug' ".  The reset of the target is pretty simple, it copy the files from uncompressed folder to content folder and add min to the filename.  Here is the xml snippet for CopyWebContent:

<Target Name="CopyWebContent" Condition=" '$(Configuration)' == 'Debug' ">
    <Js Include="uncompressed\scripts\*.js" Exclude="uncompressed\scripts\*.min.js" />
    <Css Include="uncompressed\styles\*.css" Exclude="uncompressed\styles\*.min.css" />
  <Copy SourceFiles="@(Js)" DestinationFiles="@(Js -> '$(MSBuildProjectDirectory)\content\scripts\%(Filename).min.js')" />
  <Copy SourceFiles="@(Css)" DestinationFiles="@(Css -> '$(MSBuildProjectDirectory)\content\styles\%(Filename).min.css')" />

Lastly let’s take a look at MinimizeWebContent, the meat of this article.  Before we proceed we need to add the conditional statement to MinimizeWebContent because we only want to minimize the files when we compile to project for release environment.  The main task that’s going to minimize files is the <AjaxMin> task, the task takes six arguments: JsSourceFiles, JsSourceExtensionPattern, JsTargetExtension, CssSourceFiles, CssSourceExtensionPattern, and CssTargetExtension.  We will look at the JavaScript related parameters.

First, we will create an ItemGroup for JavaScript files named Js, it will include all files under uncompressed\scripts folder.  The ItemGroup Js will be passed to JsSourceFiles argument.  Next we need to give JsSourceExtensionPattern a value, the parameter accepts a regular expression for identify extension portion of the filename.  Lastly the value assigned to JsTargetExtension will replace the file extension identified by JsSourceExtensionPattern with new file extension.  The other three parameters are similar to the ones we just discussed but are used for CSS files.  Once the files are process by AjaxMin we need to copy them to content folder and delete the generated minimized files from uncompressed folder.  The xml snippet that performs all the action described about is shown here.

<Target Name="MinimizeWebContent" Condition=" '$(Configuration)' == 'Release' ">
    <Js Include="uncompressed\scripts\*.js" Exclude="uncompressed\scripts\*.min.js" />
    <Css Include="uncompressed\styles\*.css" Exclude="uncompressed\styles\*.min.css" />
  <AjaxMin JsSourceFiles="@(Js)" JsSourceExtensionPattern="\.js$" JsTargetExtension=".min.js" CssSourceFiles="@(Css)" CssSourceExtensionPattern="\.css$" CssTargetExtension=".min.css" />
    <JsMin Include="uncompressed\scripts\**\*.min.js" />
    <CssMin Include="uncompressed\styles\**\*.min.css" />
  <Copy SourceFiles="@(JsMin)" DestinationFolder="content\scripts" />
  <Copy SourceFiles="@(CssMin)" DestinationFolder="content\styles" />
  <Delete Files="@(JsMin)" />
  <Delete Files="@(CssMin)" />

Now after all the work we can finally see the result of our labor.  To see the result set the configuration of the project to ‘Debug’ and build the project. We can then see the total file size for all the files under content/scripts and content/styles is 60,519 bytes.  Next we set the configuration to ‘Release’ and rebuild the project.  Again check to the total file size and you should see 57,544 bytes.  That’s 2975 bytes saved or 4.9%.  That’s not very impressive but we have to remember that majority of the files are already compressed and we will not get much saving from them.  To get a better picture of what our saving could be we will take a look at home_index.js file.
Here is the uncompressed version of the file, home_index.js

function ValidateHtml5Functionality() {
    var supportsAll = true;
    var validationSummary = $('#validationSummary');
    var validIcon = '<span class="ss_sprite ss_accept "> &nbsp;  </span>';
    var invalidIcon = '<span class="ss_sprite ss_cancel "> &nbsp;  </span>';
    validationSummary.append('<li>' + (Modernizr.applicationcache ? validIcon : invalidIcon) + 'Application Cache</li>');
    validationSummary.append('<li>' + (Modernizr.canvas ? validIcon : invalidIcon) + 'Canvas</li>');
    validationSummary.append('<li>' + (Modernizr.canvastext ? validIcon : invalidIcon) + 'Canvas Text</li>');
    validationSummary.append('<li>' + (Modernizr.localstorage ? validIcon : invalidIcon) + 'Local Storage</li>');
    validationSummary.append('<li>' + (Modernizr.sessionstorage ? validIcon : invalidIcon) + 'Session Storage</li>');
    validationSummary.append('<li>' + (Modernizr.webgl? validIcon : invalidIcon) + 'Web GL</li>');
    validationSummary.append('<li>' + (Modernizr.websqldatabase ? validIcon : invalidIcon) + 'Web Sql Database</li>');
    validationSummary.append('<li>' + (Modernizr.websockets ? validIcon : invalidIcon) + 'Web Socket</li>');
    validationSummary.append('<li>' + (Modernizr.webworkers ? validIcon : invalidIcon) + 'Web Workers</li>');

And here is the compressed version, home_index.min.js

$(document).ready(ValidateHtml5Functionality);function ValidateHtml5Functionality(){var d=true,a=$("#validationSummary"),c='<span class="ss_sprite ss_accept "> &nbsp; </span>',b='<span class="ss_sprite ss_cancel "> &nbsp; </span>';a.append("<li>"+(Modernizr.applicationcache?c:b)+"Application Cache</li>");a.append("<li>"+(Modernizr.canvas?c:b)+"Canvas</li>");a.append("<li>"+(Modernizr.canvastext?c:b)+"Canvas Text</li>");a.append("<li>"+(Modernizr.localstorage?c:b)+"Local Storage</li>");a.append("<li>"+(Modernizr.sessionstorage?c:b)+"Session Storage</li>");a.append("<li>"+(Modernizr.webgl?c:b)+"Web GL</li>");a.append("<li>"+(Modernizr.websqldatabase?c:b)+"Web Sql Database</li>");a.append("<li>"+(Modernizr.websockets?c:b)+"Web Socket</li>");a.append("<li>"+(Modernizr.webworkers?c:b)+"Web Workers</li>")};

For this file the original file size is 1,341 bytes, and the compressed version is 812 bytes.  That’s 529 bytes saved or 39%!  Much better result.

There you go, with this setup we can now develop and debug our JavaScript using normal formatting/whitespace rule, and deploy minimized version of JavaScript files in an automated fashion.  With Ajax enabled web so prevalent nowadays this will help you create more responsive website by reduce the number of bytes the user will have to download.  There are few more things we can do to further reduce the latency of the website.  One thing is to combine the non-page specific files into one to reduce the number of request to server, this could be accomplished by using both YUI Compress for non-page specific files and AjaxMin for page specific files.  I will leave that to you as an exercise.

Download the sample project here.