JS1k: Making a very small game in JavaScript part 2 - optimisations
This is part 2 of a series. See part 1 here.
Two types of optimisation
There are three types of optimisation when you’re trying to reduce the size of the code. The first two are “defactoring” techniques: they change the code but not the functionality. The third changes code and functionality.
1. Optimising for the compiler
One of the major optimisations the Closure compiler carries out is the renaming of identifiers. Closure will, wherever possible, rename your variables and functions with one letter names. This means you can keep using the nice long descriptive names and know that in the final file they won’t take up any more space than if you called everything x
and y
. So, how can you help with this?
One tiny optimisation I discovered was that I was using the literal 50
in a number of places. I defined a new variable fifty
in the global scope and assigned it the value 50
. Replacing all appearances of 50
with fifty
increased the size of my un-minified code, but when minified, fifty
became a single letter identifier and saved me a byte each time. Of course, there’s some extra code overhead to allow this, so there have to be enough occurrences for it to be worth it. Even a single byte’s worth it though.
I discovered I was using a.lineTo(x,y);
a lot, where a
is the canvas context. Closure can’t rename a.lineTo
, so I needed to give it something it could rename:
function lineTo(x,y) {
a.lineTo(x,y);
}
Again, there’s some overhead involved in setting this up, but with enough use of a.lineTo
it’s well worth it.
I had to work round a bug with the compiler here. When it gets to the definition of lineTo
it makes the assumption that inlining it will save space. So every occurrence of lineTo(x,y)
in the code is replace by a.lineTo(x,y)
, and it removes the lineTo
definition. To stop it doing this, I had to add the following line:
window['lineTo'] = lineTo;
This line forces the compiler to keep the lineTo
definition, and when compiled produces window.x=x;
. This is good and bad news - lineTo
is now kept (in minified form) but there’s a useless line added. The way I got round that was to add a post-compilation step to my Makefile:
sed -i 's/window\.[a-zA-Z]*=.;//g' kave-min.js
(Yes, I learned sed
for this. That’s dedication.)
2. Optimising the code
Some code optimisations just make the code size smaller, full stop. One is true
and false
. In a lot of cases, 1
and 0
will do just fine, at a quarter of the size.
Code organisation matters too. You may be used to writing nice modular code with no global variables and loose coupling between each module. All those good intentions need to be completely suppressed. Make everything a global unless it absolutely has to be local. Everything should know about everything else. Couple wherever possible.
An example of coupling comes in the rendering stage of the game loop. The context fillstyle
is set to white. After this, the snowball is rendered, followed by the snow. Then the fillstyle
is set to blue, and the icicles and walls are rendered. The order of these rendering functions is tightly coupled in terms of order of execution. Trying to render the icicles before the snow would make the icicles white, unless we changed the fillstyle
to blue then back to white for the snow.
3. Changing the behaviour
I said that the first two techniques “defactored” the code, leaving the behaviour unchanged. Sometimes the current behaviour is essentially complex, and needs to be simplified to reduce its code size. The simplest solution is to just leave out functionality, but sometimes that’s a compromise too far. Following are some less drastic options.
Here’s a really simple example. I picked a nice blue for the icicles with the following line:
a.fillStyle = "rgb(190,230,255)";
Unfortunately, the syntax for writing arbitrary rgb colours is relatively verbose. Much shorter are the colour names, like “yellow” or “blue”. “blue” was the wrong colour, but using Doug Crockford’s nice CSS colour chart I was able to find “lightblue”, which was pretty close to the original colour, but saved me 7 valuable bytes over “rgb(190,230,255)”.
The snow falls diagonally, but with a little bit of added randomness. I wanted it to follow a sinusoidal curve as it fell, but the additional code to enable this was too long, so I cut it. The snow’s not as natural as I’d like, but I had to compromise.
The collision detection algorithm’s pretty simple. I used the context.isPointInPath(x,y)
method, passing the front middle point of the snowball. I could then call the detectCollision()
function every time I drew a new path that I wanted to check (i.e. the walls and the icicles). It would be nice to check the top, front and bottom points of the snowball, but that would have been too much code.
Don’t try this at home
Coding is usually about weighing things up: readability/maintainability, execution speed, memory use, code size, etc. When you’re doing a JS1k submission, the only one of these that matters is code size (well, execution speed can matter as well, but only as a secondary concern). The important thing to remember though, is that this kind of optimisation is wrong, just plain wrong. It’s almost never a good idea to sacrifice maintainability for code size. If it were, we’d all be writing Perl. I’ll leave you with that thought.