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">
  <ItemGroup>
    <JsToDelete Include="content\scripts\**\*.*" />
    <CssToDelete Include="content\styles\**\*.*" />
  </ItemGroup>
  <Delete Files="@(JsToDelete)" />
  <Delete Files="@(CssToDelete)" />
  <CallTarget Targets="MinimizeWebContent;CopyWebContent" />
</Target>

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' ">
  <ItemGroup>
    <Js Include="uncompressed\scripts\*.js" Exclude="uncompressed\scripts\*.min.js" />
    <Css Include="uncompressed\styles\*.css" Exclude="uncompressed\styles\*.min.css" />
  </ItemGroup>
  <Copy SourceFiles="@(Js)" DestinationFiles="@(Js -> '$(MSBuildProjectDirectory)\content\scripts\%(Filename).min.js')" />
  <Copy SourceFiles="@(Css)" DestinationFiles="@(Css -> '$(MSBuildProjectDirectory)\content\styles\%(Filename).min.css')" />
</Target>

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' ">
  <ItemGroup>
    <Js Include="uncompressed\scripts\*.js" Exclude="uncompressed\scripts\*.min.js" />
    <Css Include="uncompressed\styles\*.css" Exclude="uncompressed\styles\*.min.css" />
  </ItemGroup>
  <AjaxMin JsSourceFiles="@(Js)" JsSourceExtensionPattern="\.js$" JsTargetExtension=".min.js" CssSourceFiles="@(Css)" CssSourceExtensionPattern="\.css$" CssTargetExtension=".min.css" />
  <ItemGroup>
    <JsMin Include="uncompressed\scripts\**\*.min.js" />
    <CssMin Include="uncompressed\styles\**\*.min.css" />
  </ItemGroup>
  <Copy SourceFiles="@(JsMin)" DestinationFolder="content\scripts" />
  <Copy SourceFiles="@(CssMin)" DestinationFolder="content\styles" />
  <Delete Files="@(JsMin)" />
  <Delete Files="@(CssMin)" />
</Target>

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

$(document).ready(ValidateHtml5Functionality);
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.

No comments: