Optimizing cross-compiled JavaScript

There are three topics left that I would like to touch on next:

  • optimizing
  • debugging
  • testing

Many of you probably think that optimizing JavaScript is mostly about minifying the code thus keeping code size small and load times short. Minifying is a big part of optimizing cross-compiled ActionScript but there is also a little bit more to it.

 

Picking the right Optimizer

There are many code minifiers out there but I think everybody will agree with me when I say that  Google’s Closure Compiler is the best. The Closure Compiler offers three different optimization modes and it is worth studying what does what:

  • WHITESPACE_ONLY
  • SIMPLE_OPTIMIZATIONS
  • ADVANCED_OPTIMIZATIONS

SIMPLE_OPTIMIZATIONS is the default and it always works. What you want is ADVANCED_OPTIMIZATIONS. But here is the thing: Google does not guarantee you that your optimized code will always run after optimizing with ADVANCED_OPTIMIZATIONS. In order to successfully optimize with ADVANCED_OPTIMIZATIONS you need to follow a list of rules that you should perhaps follow anyways.

 

Ninja in, Turtle out

For example calling member functions by name is a no-no for ADVANCED_OPTIMIZATIONS:

// ActionScript
var sprite : Sprite = new Sprite();
var width : Number = sprite["width"].apply(sprite,[]);

Who would do that kind of stuff? I guess, you can call them ActionScript Ninjas, but you will also find this kind of style in Adobe’s Flex libraries. I don’t recommend this coding style and it will for sure limit your optimization options to SIMPLE_OPTIMIZATIONS and  WHITESPACE_ONLY. In general trying to write “cute” ActionScript code will almost always backfire. Either your cross-compiler will misinterpret what you want to do, or the Closure compiler will generate optimized code that does not run. Trust me: reality will bend every ActionScript Ninja. You will be faced with the choice of being right (“This is allowed in ActionScript!”) and being fast (“Hmm, I like 60 fps!”). You pick!

Just to give you an idea what ADVANCED_MODE is capable of if you don’t feed it ninja code: A year ago I cross-compiled Mike Chambers’s Pew Pew game to JavaScript and recently beefed it up a little bit (by also removing some ninja parts). The uncompressed, cross-compiled JavaScript ended up being about 1.5 MB. The optimized version using ADVANCED_MODE brought the size eventually down to 116 KB. I would be willing to give up ninja-style programming for those kind of improvements!

 

Honey, I shrunk the code

But how? And why was Pew Pew so big to begin with (1.5 MB)? I am planning on writing a series of posts about cross-compiling Pew Pew to JavaScript shortly, but this seems the right time to explain what I think you should consider in your design of your cross-compiler. Simply put, your cross-compiler should try to support ADVANCED_MODE as best as it can. One of the  ADVANCED_MODE rules is that you have to annotate your code with type information (technically you don’t have to, but you want to as explained above):

// ActionScript
package goog
{
  class Baz
  {
    public function query(groupNum:Number, term:Object=null):void { ... }
  }
}
// JavaScript
/**
 * Queries a Baz for items.
 * @param {number} groupNum Subgroup id to query.
 * @param {string|number|null} term An itemName,
 *     or itemId, or null to search everything.
 */
goog.Baz.prototype.query = function(groupNum, term) {
  ...
};

(Source: http://code.google.com/closure/compiler/docs/js-for-compiler.html)

I am proposing that an ActionScript to JavaScript compiler should always emit all type annotations to the JavaScript code in order to enable optimizers like Google’s Closure Compiler to using those type annotations for enhanced optimizations.

 

Boring is beautiful

You might not always succeed with your noble plan, though. For example it is extremely difficult to determine at compile time what the set of types will be for query’s “term” parameter. So I suspect instead of:

* @param {string|number|null} term An itemName,

You will probably only be able to emit:

* @param {object|null} term

But that’s better than nothing! To me this example is just a variation of the “ninja-in, turtle out” problem. May I ask you: Do you really have to use Object as the type for “term”? How about this “less cute” version of query():

// ActionScript
package goog
{
  class Baz
  {
    public function queryByNumber(groupNum:Number, term:Number=NaN):void { ... }
    public function queryByString(groupNum:Number, term:String=null):void { ... }
  }
}

That’s right: disambiguate your API by using separate methods for string terms and number terms. I hear some ninjas protesting loudly and arguing that my chatty coding style will result in larger code. Trust me, it will not. The ADVANCED_MODE will take of it. But if you use Object and Array (instead of Vector) all over the place the Closure compiler might not be able to make any sense of your code and you will end up with large chunks of half-optimized code. Or consider this example: maybe nobody uses queryByString()? In my chatty version the closure compiler would strip out that function, which it couldn’t do so for the do-it-all-in-one query() method. In this case, I am afraid, boring is beautiful.

 

Connecting the dots

So far I have only been talking about strategies for minifying generated JavaScript. At this point I might have also convinced you to not worry too much about the code size of  your generated, unoptimized JavaScript. We expect it to be large and verbose, because of the extra type annotations and unused methods that the ADVANCED_MODE will hopefully take care of later.

But there is one type of optimization that you have to take care of yourself. Consider this example from a previous post:

// ActionScript:
package flash.display
{
    public class Sprite
    {
    }
}
// JavaScript:
var flash = {};
flash.display = {};
flash.display.Sprite = function() {};

Creating a Sprite should result in this generated JavaScript code:

// ActionScript:
var sprite : Sprite = new Sprite();
// JavaScript:
var sprite : Sprite = new flash.display.Sprite();

What could possibly be wrong with that translation? There is nothing wrong with it, it’s just slow. The Closure compiler will optimize the JavaScript snippet above roughly to this:

var a=new b.c.d();

Do you see the problem? You would like to get this instead, don’t you?

var a=new b();

With our current implementation of the cross-compiler using too many packages and classes results in code that unnecessarily dereferences object parts where the parts will be optimized but not the expression as a whole. Bummer. Here is my proposal to address this problem (which is unfortunately not easy to implement): Introduce a package separator constant that is a string and that defaults to “.” in debug mode. In release mode use something like “$”. What the closure compiler will receive from your cross-compiler will then look like this (without the type annotations):

// JavaScript:
var flash = {};
var flash$display = {};
var flash$display$Sprite = function() {};
var sprite : Sprite = new flash$display$Sprite();

Since “flash$display$Sprite” is one name literal the Closure compiler will now happily optimize your JavaScript to:

var a=new b();

Here are some numbers to finish up this post: cross-compiled Pew Pew with “.” as package separator yielded about 205 KB optimized JavaScript while the “$” version made the app faster (thanks to less dereferencing) and reduced the size further down to 116 KB.