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
- Install Java on your build/development machine
- Add the Java executable to the PATH variable
- Install the MetroClosure NuGet package to your project
- 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:
- The MetroClosure package will append to the Build Log any situations where the compiler can’t optimize your code.
- Javascript Compilation is restricted to RELEASE configuration, since it can take time to produce
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.