Minifying JavaScript Files with Build Events

Introduction

As any good web developer will tell you, having a lot of JavaScript can impact performance of a web site. One best practice for mitigating this problem is to minimize the size of JavaScript files.

But it is not always as simple as just "compiling" the script into minified versions. At some times, the minified scripts are all that is necessary while at other times, having the full script file (including comments) is useful. In the SharePoint world, we often see the latter. The JavaScript published by Microsoft includes the minified files (core.js, sp.ribbon.js) as well as the raw, expanded versions (core.debug.js, sp.ribbon.debug.js). The appropriate version of these files is rendered by the server based on the setting in web.config () or the tag via the debug attribute. In my project, we are following this paradigm by packaging both versions of the JavaScript as we develop the site.

My first stop, and the topic of this post, is the processing of the script to reduce its size. Since every byte of the file needs to be transmitted from the client to the server, every bit should be necessary. In the JavaScript world, this is called minification, since the file is minimized. I need to point out that most of the information in this post applies to any Visual Studio project file - not just SharePoint and not just CSharp.

Since I don't want to write JavaScript in minimized mode by hand, I set off looking for a tool.

Approaches

As I consideredthe possible approaches to this scenario, I had a few limitations/requirements that apply due to various restraints. There is no money available to license a third-party tool; the minification process needs to run automatically and as seamless as possible; I want developers to accomplish everything inside Visual Studio; and any tools/configuration needs to be inside the source control repository.

As a result, I had three alternatives to consider:

  • Custom Tool that operates on the file
  • Pre/Post events in Visual Studio
  • Custom task in the build process

Custom Tool inside Visual Studio

Whenever I need a tool for something in SharePoint development, I immediately look to Waldek. (Twitter followers already know about my undying love for Waldek. ? ) Sure enough, he has written a Visual Studio extension to minimize a JavaScript file using a "Custom Tool" in Visual Studio - Mavention SharePoint Assets Minifier. I will let you read all about that extension directly from Waldek himself, just come back when you finish. (Please!) http://blog.mastykarz.nl/minifying-javascript-css-mavention-sharepoint-assets-minifier/

For some as-yet-undetermined reason, the extension did not run on a machine of one of the members of my team. (And, you guessed it, he is helping with the javascript.) This got me thinking, however, that a custom tool on a file in Visual Studio is only run when the file is saved in Visual Studio by someone with the tool installed and working. In a distributed development environment, I cannot make that assumption. Perhaps a developer has an issue with the extension. Perhaps she doesn't use Visual Studio to write JavaScript. Perhaps a file is added to the project but never edited? In these scenarios, we need to take a different approach. As you can imagine, the minification of the files should happen when the SharePoint solution (wsp) is built.

Pre/Post events inside Visual Studio

There are two sets of pre/post events in SharePoint projects - pre/post build and pre/post deployment. Obviously, the pre/post deployment events will not work, since they run after the wsp file is generated. The pre/post build events are promising though. They run before the packaging process, they are seamless to developers, and the settings are stored within the project file - allowing the settings to be stored in source control.

Custom task in build process

All of the benefits of the pre/post build events also apply to the build process. There is one drawback, however. The build process typically operates on a build server after code is committed. I want the minification to happen on the developer's machines. But, if the pre/post build event is coded appropriately, they can also run on the build server.

Implementation

So, how are we minifying our file? I am using the pre-build event in Visual Studio. To also leverage the build server, I created a target directly in the .csproj files and that target is part of the pre-build event processing. The remainder of this post will cover the implementation details.

Modifying the project file

Before proceeding any further, I should note that many of the following changes are not performed using Visual Studio dialogs/windows. These changes are updates to the project file (*.csproj) which is actually an xml file that uses the MSBUILD schema. The process requires the project to be unloaded in Visual Studio, edited and then re-loaded.

To help illustrate the changes to the project file, I started with a new, empty SharePoint project, and then added a Visual Web Part (Sandboxed). I then unloaded the project and opened the .csproj file in the XML editor, collapsing all of the PropertyGroup and ItemGroup elements. Since the order of entries in the file does not matter, I will add all customizations at the bottom:

Items to minimize

The major obstacle I had to overcome was the naming of the files. The "source" file name ends in ".debug.js" and all of the metadata variables %(variable) in MSBUILD that reference an extension will only select the ".js" part. Do you know how hard it is to remove a string from the middle of a filename in MSBUILD? Yea, it is that difficult. And it drove me nuts! And, just when I thought I was getting close, it occurred to me that using the web-industry standard naming in which the minified file ends with ".min.js" is perfectly acceptable as well. So, I backed out of that approach and decided that the developer should specify the files to minimize in a different manner.

The "Build Action" property of a file can be assigned to one of several pre-defined values. These values can be extended via a setting in the project file, as described on MSDN in the MSBUILD/Visual Studio integration page:

Additional Build Actions

Visual Studio allows you to change the item type name of a file in a project with the Build Action property of the File Properties window. Compile, EmbeddedResource, Content, and None item type names are always listed in this menu, along with any other item type names already in your project. To ensure any custom item type names are always available in this menu, you can add the names to an item type named AvailableItemName.

So, I added that item group to the project, changing "JScript" to "JavaScript". A side effect of this change is an item in the solution explorer named "JavaScript". We will deal with that later.

Any file that require minification must then be assigned the type "JavaScript".

Once you have an item assigned to our JavaScript build action, we can remove the JavaScript item from solution explorer (right-click, delete). When the project is opened in the future, the Build Action list is populated with items that are found in the project. So, having at least one item in the project will ensure that it remains in the Build Action list.

Let's look again at our project file:
Lines 89 and 95 are our javascript files. And the AvailableItemName entry is no longer in the file.

Build Target

Now that we have identified the files to process, we can code a build target to select those items. A build target is not normally specified in a project file, rather the standard targets are imported from the files shipped with Visual Studio. However, we are free to add whatever we need. Let's look at a target that simply lists the files that match our criteria.

When the Build Action of a file is changed to our new value of JavaScript, the ItemName element name of that file is changed inside the project file. We can then refer to the JavaScript files using the MSBUILD item collection syntax. The following will list all of the items with the JavaScript build action/element name:

The target we've created shows a semi-colon separated list of items in the project that have the JavaScript build action. Looking good!

Minimized file name

Now that we can identify the files to minimize, we are back at the problem I started with. What is the filename of the resulting minified file? Again, using Waldek's tool as inspiration, I started digging thru the MSBUILD/Visual Studio documentation. The Common MSBUILD Project Items page details the attributes of different item types (Build Action in VS Properties). For Content Items, two of the common attributes have relevance to our approach; DependentUpon and LastGenOutput. The following image shows how DependentUpon nests the output file below the input file:
Under the covers, the output files (DialogHandler.min.js & VerbClientHandler.js in the picture above) are actually added to the project file as new elements, and the attributes are applied to them. The LastGenOutput attribute is applied to the input files, neatly tying the related items together (lines 89-95 and 101-107):

So, by running Waldek's custom tool against all the necessary javascript files, or by manually modifying the project file, I will have a set of files to pass into my script compressor (the JavaScript build action), and I have the resulting filename for the output (the LastGenOutput) attribute. Once the minified files are added to the project, we need to verify that they are configured as a TemplateFile so that they are included in the SharePoint solution package. If your original file is in the LAYOUTS mapped folder, this will happen automatically. Let's modify our build target to show us both of these filenames:

Behind the scenes, the addition of the item metadata attribute (item metadata uses the "%" symbol) to the target causes MSBUILD to treat the items differently. In the first version of the target, the Message task is run once listing every item in the item collection @(JavaScript). In the second version, the Message task is run once for every unique combination of metadata. This change in processing works in our favor, since we want to minify each file individually. This concept is described as MSBuild Batching

Build Task

Now that we can identify our input files and their output filenames, we need to actually perform the minification. Waldek's code is not a build task, but searching CodePlex yields a build task that uses the same script compressor: http://yuicompressor.codeplex.com/. The documentation on that site is pretty complete; however, the documentation is for version 1 of the code, while the default release is version 2. In addition, version 2 does not include a compiled version of the build task. So, either download the source, or version 1.7 for .Net 3.5 (which is what I did). Remember - SharePoint 2010 runs on .Net 3.5.

I created a second target in my project file to actually perform the compression. (Leaving the original target is helpful for debugging.) I've tweaked the selection of items a bit, to include only JavaScript items with the LastGenOutput set. Since we are using LastGenOutput for our output file name, it cannot be blank.

Let's take a look at the source and output files:

Calling the task from Visual Studio pre-build

The last step is to ensure that our new target is actually invoked when the project is compiled. The CodePlex site shows how to create a post-build task that launches MSBUILD to process a separate file. I used a very similar alternative - overriding the built-in Before Build target (lines 130-132):
(By default, a csproj file will not include the BeforeBuild target. The MSBUILD process will invoke that target, if it is present: Extend the Visual Studio Build Process.)

Important Note:

We need to ensure that the build task assembly, and all of its dependencies, are copied to all machines that will build the solution. I recommend creating a folder in the project named lib (short for library) to contain all assemblies required by the project. Third-party assemblies (controls/components) would usually go in this library as well.

If a machine running a build (either within Visual Studio or with the msbuild.exe program) and the build references a task assembly that cannot be found, the build will fail. Even if the compile is successful, the build will fail.

Summary

Thisarticle shows how to integrate an external tool for compressing javascript into the Visual Studio environment for SharePoint projects. (Although, very little is SharePoint specific.) The MSBUILD engine is very powerful and customizable - even without the fancy build workflow as part of TFS Server. I recommend that you explore the capabilities of MSBUILD the next time you encounter a pain point in Visual Studio.