Writing a JS1k demo

2016-04-22

With JS1k behind us and the jury making up their mind I thought it'd be nice to show you an overview of how this golfing thing works and why I think it can be a lot of fun.

Note: A client of mine asked me to write blog posts for them. This is the one of them. You can find the original post on their website, linden-it.com. You can find a copy below for posterity (was added here at least a month later).

BE AWARE! The tips in this post only apply to writing demos like those for a competition called JS1k, where the goal is to save space. THESE TRICKS ARE NOT MEANT TO BE USED IN PRODUCTION. EVER. SERIOUSLY.

For this post I'll use my own demo, Dotted Elements. It's a demo that started at about 4810 bytes. You can ignore the shim boiler plate in that file, it will not appear in other commits, I think. I golfed it down, and back up, and then down again to be under 1024 bytes after post processing my result with a special minifier.

The code golfing process, which lends its name from the sport golf where you have to putt the ball into a hole with as few strokes as possible, starts with the "simple" stuff. The simple stuff being whitespace elimination, variable name shortening, and removing some trivial structures that you would normally use but you don't need for a "demo". Actually, that's not what you start with. These are the tasks you can automate. There are plenty of production grade tools, called "minifiers", available to do this for you so it's not where you start.

A common beginner mistake I see is trying to stay correct. Things like properly declaring your vars with var, wrapping your demo in an anonymous function, and even proper structures like if-else or even while statements. That is exactly what you should be doing in production. And that is exactly what you are going to be ditching for JS1k. Minifiers won't do this for you, that's not what they were designed to do.

For var that is fairly easy; Just remove any occurrence of the var keyword. This is often safe on its own (in this demo context), but in case of "shadowing" you'll have to make sure variable names don't clash.

Eliminating if-else means replacing if (x) y; else z; with the so called "ternary expression" and ends up being x ? y : z;. The difference is 8 bytes. Per occurrence. And zero difference in semantics. Additionally, the result is now an "expression", which opens up new vectors for minification.

One of those vectors could apply to optimizing the while structure. Note that while(x)... has the same length as for(;x;).. loop, both are 8 bytes. But the for loop allows you to add two expressions "for free" inside the for-header. In this context, "for free" means you don't have to add a semi-colon (;) or comma (,) for them. To combine that with the if-else, take this example: if (a) b; else c; while(x) y;. We can combine and rewrite that to: for(a?b:c;x;)y; and we save ourselves the 8 bytes of the if-else reduction AND the semi-colon for the statement. Result will still only execute that conditional only once and the loop still does the same thing. Note that it also has a "free" spot left for something to happen in the loop, so you may be able to rewrite it as for(a?b:c;x;y); as well. But that particular rewrite only saves at all if it happens to be the very last line of the script, or if there are more lines inside the loop besides y, to save a semi/comma. So you see, it's not all that straightforward.

After eliminating formal structures you continue by finding code that repeats and eliminating it. You may discover that you'll undo this partially later but we can circle back to that. Examples like this are to find cases of Math.xxx and cache them. All the properties on the Math global are in fact "static". That means they can be cached locally so you don't have to repeat them. So Math.min(); Math.pow(); becomes m=Math.min; p=Math.pow; m(); p();. But only if it actually saves because when you only use Math.pow once caching it only means adding more bytes. So there's a trade-off and you have to verify that it saves you anything.

Likewise, property names can be cached as well. The property names of a canvas context can be quite lengthy, with fillStyle, clearRect, and clearRect and such. If you use some property name often enough you could put it in a variable as a string and use dynamic lookup; s = 'fillStyle'; c[s ](..);. Again, there is some overhead incurred for doing this so you have to check each case to make sure it actually saves you anything. Often two or three occurrences can suffice, but the shorter the name the more you're likely to need.

The next step in this could be to inject some with statements in the mix. This should not be taken too lightly though as it can drastically affect the performance of your demo. But instead of doing = Math.pow; s = Math.PI; you could also do (Math) { pow(); PI; }. This will work fine but has performance issues as the dynamic lookup is non-trivial for browsers. And since the overhead is bigger you need more occurrences to make it viable at all.

But why even go for Math.PI in the first place? Are you sure 3.14 won't suffice for your demo? Or maybe even 3.1 (or 3.2)? Or heck, why not just 3 as an approximation. Are you sure your viewers are going to see any difference? It will save you up to six bytes! If that seems like negligible you clearly haven't been golfing hard enough :) But seriously, "fake it till you make it" is an important area of golfing. It's often rewarding to just replace computations that look complex by simple constants or by reusing other values. Do you really need five calls of Math.random() or will it suffice to cache that value once per loop? Sometimes it matters, often it won't.

Invert logic structures if that saves you space. If you're doing if (!x) y; else z; anywhere you should be doing if (x) z; else y; and eliminate that useless exclamation mark.

A more advanced trick is to use bitwise operators in logic conditions. If a and b are booleans (true or false) you could replace if (a && b) ...; with if (a & b) ...; because the end result would only pass the condition if both vars are true. Because those bools are converted to 0 or 1, which means it's doing 1&1, which would evaluate to truthy or falsy. And it saves you a byte at no real expense.

There are many more tricks to cover and this article doesn't have the space to cover them all so let's head over to what happens next. Let's say you golfed your demo down from 3k down to 1.5k. That's still some 500 bytes too much! Now what? Enter the "Regpack" minifier. Regpack is basically gzip for JS1k. It started as a JS1k demo itself ("jscrush") and evolved quite a bit since then. It views your source as a string and will try to find string repetitions to replace as efficient as possible. This can often save you a few hundred bytes, depending on how you've golfed down your demo.

Using something like Regpack means you need to golf slightly different though. You're not only looking for the shortest way to get something done but you also need to try and be as repetitive as possible, while still keeping in mind that each repetition will still cost you at least one byte. There are no free lunches. But still, the savings can be huge. Remember that I initially told you to cache multiple Math.pow() calls into p = Math.pow; p(); p();? Well, don't. At least, not if you use Regpack. It will see those repetitions and do a similar replacement for you, except this replacement happens as a string, so there's no "temporary variable". The overhead still exists, to some degree, but it's harder to measure as it's now part of the generic overhead for the packer and not as easy to quantify. Basically, by using the tool the replacement would happen anyways so you may as well make use of it. It will be more efficient than doing it manually.

The packer comes with its own overhead of course. Not sure what it is right now, but since it uses regular expressions and eval, adds some bytes for replacement, etc. There's a limitation of 1024 bytes and that's why that particular size constraint seems so perfect; it's enough to allow this kind of size reduction, but not so much to "just apply gzip". There's a subtle balance between what kind of overhead leads to rewarding results. But do be warned; using a tool like Regpack does not eliminate the need for golfing down your demo and relying on it for those last bytes puts you in a casino mode: Not all changes may benefit your demo after packing even if they do before. This can lead to very frustrating results. But it's so rewarding once it does work!

I'm Peter and I run the JS1k competition. Code golfing can be a lot of fun, especially if you're into logic puzzles. But it's not everybody's cup of tea, and that is fine too. Next competition will be next year so plenty of time to prepare ;)