Optimize your WinJS with MetroClosure

Want a boost on your WinJS application performance for free?

Download my NuGet package MetroClosure to integrate the power of the Google Closure Compiler into your WinStore packages.

Supports both standard WinStore Projects and Universal Apps for WinStore/WinPhone

How to Setup

  1. Install Java on your build/development machine
  2. Add the Java executable to the PATH variable
  3. Install the MetroClosure NuGet package to your project

  1. Build in RELEASE configuration

A Sample Optimization

Let’s say we build an application that does a long-running computation on launch:

var onStart = function () {
 	    // Dispatch
 	    setTimeout(function () {
 	        var outputContainer = window.document.getElementById('Main');
 	
 	        // Start timer
 	        var start = new Date();
 	
 	        // Do some work that is not optimized
 	        var dict = {};
 	        for (var i = 0; i <= 5000; i++)
 	            for (var m = 0; m <= 300; m++) {
 	                dict[i.toString() + "-" + m.toString()] = i + m;
 	            }
 	        var total = 0;
 	        for (var key in dict) {
 	            total += dict[key];
 	        }
 	
 	        // Stop timer
 	        var end = new Date();
 	
 	        // Output results to screen
 	        outputContainer.innerText = end.getTime() - start.getTime() + "ms";
 	    }, 0);
 	}
 	

Javascript Gurus (like myself) can see immediate problems with this code. For starters, the for loops are a source of major cost and not all loop styles perform the same across browsers.

A few optimal examples from Chrome:

And a poor performing:

Running the above example in DEBUG, which doesn’t use the closure compiler, will clock at about ~5000ms in the Win8.1 runtime.

But, look at what the code is refactored to when building in RELEASE, which clocks at around ~3500ms:

for(var b=window.document.getElementById("Main"),e=new Date,a={},c=0;5E3>=c;c++)
 	    for(var d=0;300>=d;d++)
 	        a[c.toString()+"-"+d.toString()]=c+d;
 	    for(var f in a);
 	        b.innerText=(new Date).getTime()-e.getTime()+"ms"
 	

The Google Closure Compiler actually compiles the JS, optimizes and then re-serializes out to the most commonly performant code.

You get JS optimizations free with this tool, so generally you don’t have to do anything special during development-time.

Usage Notes:

How does it work?

Well, for starters, the MetroClosure package hooks into your principle Project File via an MSBuild Import statement.

 <Import Project="packages\MetroClosure.0.1\build\win\metroclosure.targets" Condition="Exists('packages\MetroClosure.0.1\build\win\metroclosure.targets')" />
 	  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
 	    <PropertyGroup>
 	      <ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
 	    </PropertyGroup>
 	    <Error Condition="!Exists('packages\MetroClosure.0.1\build\win\metroclosure.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\MetroClosure.0.1\build\win\metroclosure.targets'))" />
 	  </Target>
 	

When it comes time to produce an AppX, either in Visual Studio or on the Command Line, the package produces “*.cache” files alongside your scripts that contain the compiled sources.

Now, the real magic is hooking into the WinStore MSBuild default Targets. When the AppX strategy fires up, we hook into the package descriptors, remove existing JS file references and replace those with our Cache version mapping to the same target output.

<?xml version="1.0" encoding="utf-8"?>
 	<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 	  <PropertyGroup>
 	    <DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
 	  </PropertyGroup>
 	  <Target Name="CleanCacheRecords" >
 	    <ItemGroup>
 	      <CacheFiles Include="$(ProjectDir)\**\*.cache" />
 	    </ItemGroup>
 	    <Delete Files="@(CacheFiles)" />
 	  </Target>
 	
 	  <Target Name="CompileCacheRecords" DependsOnTargets="CleanCacheRecords">
 	    <ItemGroup>
 	      <JsFiles Include="$(ProjectDir)\**\*.js" Exclude="$(ProjectDir)\bin\**\*.*" />
 	      <ClosureCompiler Include="$(ProjectDir)Packages$(PackagesDirectory)\MetroClosure.*\lib\win\GoogleClosure.*\*.jar" />
 	    </ItemGroup>
 	    <Exec Condition="'$(Configuration)' == 'Release' " Command="java -jar @(ClosureCompiler) --language_in=ECMASCRIPT5 --js %(JsFiles.Identity) --js_output_file %(JsFiles.RootDir)%(JsFiles.Directory)%(JsFiles.Filename).js.cache" ContinueOnError="WarnAndContinue" />
 	  </Target>
 	
 	  <Target Name="_PackageExtraFiles" DependsOnTargets="CompileCacheRecords">
 	    <Message Text="PreCompilation-Files: %(PackagingOutputs.Identity)  TargetPath:%(PackagingOutputs.TargetPath)"  Importance="high" />
 	    <ItemGroup>
 	      <_AddToPackageFiles Include="$(ProjectDir)\**\*.cache" Exclude="$(ProjectDir)\bin\**\*.*" ></_AddToPackageFiles>
 	      <PackagingOutputs Remove="@(PackagingOutputs)" Condition="'%(Extension)'=='.cache'"></PackagingOutputs>
 	      <PackagingOutputs Remove="@(PackagingOutputs)" Condition="'%(Extension)'=='.js' AND '$(Configuration)' == 'Release' AND Exists('%(RootDir)%(Directory)%(Filename).js.cache')"></PackagingOutputs>
 	      <PackagingOutputs Include="@(_AddToPackageFiles -> '%(FullPath)')">
 	        <OutputGroup>Content</OutputGroup>
 	        <ProjectName>$(ProjectName)</ProjectName>
 	        <TargetPath>%(RecursiveDir)%(Filename)</TargetPath>
 	      </PackagingOutputs>
 	    </ItemGroup>
 	  </Target>
 	
 	  <Target Name="PackageExtraFiles" DependsOnTargets="_PackageExtraFiles" AfterTargets="GetPackagingOutputs">
 	    <Message Text="PostCompilation-Files: %(PackagingOutputs.Identity)  TargetPath:%(PackagingOutputs.TargetPath)"  Importance="high" />
 	  </Target>
 	</Project>
 	

The observant will notice a required, yet severly undocumented property left over from the C++ days:

<PropertyGroup>
 	    <DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
 	</PropertyGroup>
 	

This is required in order to prevent Visual Studio from triggering ‘Hot Deploys’ while in DEBUG. These special type of deploys actualy don’t call your Project’s Build File, making pre-processing impossible outside of the project workspace.

Looking at the RELEASE build log, you can see first the AppX manifest’s file relationships; these identify the source file (workspace) and how the resource will show up in the target package:

PreCompilation-Files: \WindowsApp-SlowCode\bin\Release\ReverseMap\resources.pri  TargetPath:resources.pri
 	PreCompilation-Files: \WindowsApp-SlowCode\default.html  TargetPath:default.html
 	PreCompilation-Files: \WindowsApp-SlowCode\images\logo.scale-100.png  TargetPath:images\logo.scale-100.png
 	PreCompilation-Files: \WindowsApp-SlowCode\images\smalllogo.scale-100.png  TargetPath:images\smalllogo.scale-100.png
 	PreCompilation-Files: \WindowsApp-SlowCode\images\splashscreen.scale-100.png  TargetPath:images\splashscreen.scale-100.png
 	PreCompilation-Files: \WindowsApp-SlowCode\images\storelogo.scale-100.png  TargetPath:images\storelogo.scale-100.png
 	PreCompilation-Files: \WindowsApp-SlowCode\js\default.js  TargetPath:js\default.js
 	PreCompilation-Files: \WindowsApp-SlowCode\css\default.css  TargetPath:css\default.css
 	PreCompilation-Files: \WindowsApp-SlowCode\packages.config  TargetPath:packages.config
 	

The \WindowsApp-SlowCode\js\default.js maps to js\default.js which we simply want to update, by altering the existing ItemGroups, to pull from our compiled `\WindowsApp-SlowCode\js\default.cache.js’.

After the MetroClosure MSBuild jazz goes through, here is what the package manifest looks like:

PostCompilation-Files: \WindowsApp-SlowCode\bin\Release\ReverseMap\resources.pri  TargetPath:resources.pri
 	PostCompilation-Files: \WindowsApp-SlowCode\default.html  TargetPath:default.html
 	PostCompilation-Files: \WindowsApp-SlowCode\images\logo.scale-100.png  TargetPath:images\logo.scale-100.png
 	PostCompilation-Files: \WindowsApp-SlowCode\images\smalllogo.scale-100.png  TargetPath:images\smalllogo.scale-100.png
 	PostCompilation-Files: \WindowsApp-SlowCode\images\splashscreen.scale-100.png  TargetPath:images\splashscreen.scale-100.png
 	PostCompilation-Files: \WindowsApp-SlowCode\images\storelogo.scale-100.png  TargetPath:images\storelogo.scale-100.png
 	PostCompilation-Files: \WindowsApp-SlowCode\css\default.css  TargetPath:css\default.css
 	PostCompilation-Files: \WindowsApp-SlowCode\packages.config  TargetPath:packages.config
 	PostCompilation-Files: \WindowsApp-SlowCode\js\default.js.cache  TargetPath:js\default.js
 	

We are just updating the package link, which makes sure our optimized goodies get into the final Appx package.

Sources and Feedback

Github NuGet-MetroClosure Library

NuGet MetroClosure Package

File an Issue