JS1k demo post-mortem


I created a demo for the JS1k'16 competition and committed each golf stroke into a git repo. I was hoping it could log the excitement of golfing down a demo but to be honest, even looking back at the commits myself I can't say the excitement is oozing out of it. That's very unfortunate but at the same time, it is what it is.

The actual demo, "Dotted Elements", can be found here.

I started commenting on individual commits but it really takes a lot of time and near the end I wasn't convinced the comments were really adding anything to the commit itself. I'll post it anyways but know it's incomplete. At least I made the PR thing so you can easily skip from one commit to the next, in order.

The demo

The initial demo landed on this commit. During the golfing some features were changed and added but in general this was the starting point. I won't bother explaining the demo itself in this post, the initial code has some comments and is quite explicit. That'll change, let's see how :)

The list of commits is put in a PR. This way github will create prev/next links to each commit for easy navigating. I'll start to discuss each commit, for as far as I can remember what's going on (it's been half a year between the time of writing the demo and writing this post mortem).

Commit 1; replaced a ceil(n) with a floor(n + 1). This way floor repeats more often and maybe we can leverage that to our advantage. Standard golf trick.

Commit 2; follow up; removes the ceil and squashes the condition addition into a ternary.

Commit 3; trivial squashing of statements to an expression.

Commit 4; eliminate var keywords. At this point I'm relying on the IDE to warn me for duplicate usage and highlighting. This prepares the demo to have its variable declarations stripped. Consolidating the vars also helps with mixing them while golfing.

Commit 5; mark a free space (the third position in the loop header is now unused). Eliminate unnecessary parens.

Commit 6; use that free space by moving an assignment into it. This is not optimal yet, it prepares for future steps.

Commit 7; rename a property to a single char (a bit prematurely perhaps, but that's fine), rewrite that if.

Commit 8; use the loop counter as instead of a separate counter. This means we can eliminate the variable. Doh.

Commit 9; turns out `ctx.fillText` doesn't expect a string; numbers are fine too. (And yeah, the `String(n)` was too big anyways, that much should be obvious.

Commit 10; oh hey the font size doesn't need to be an integer

Commit 11; save a byte by being slightly less presise. Obviously this kind of "fake it till you make it" trick doesn't always work, but in this case 0.1 instead of 0.08 works well enough. The 0.08 was meant as an "8% of height" so I grew it to 10% and wiggled it a little (later).

Commit 12; move some initializations together. This happens more often later. It helps with figuring out optimizations while golfing.

Commit 13; simple calculus, move terms inside and eliminate parens. Static computations should always be squashed when golfing.

Commit 14; squash some code together. Saves the semi-colon byte by assigning it on the first use rather than before.

Commit 15; while caching computations is usually shorter you do need to use it often enough to make it save anything. In this case twice isn't enough because it's n=m/k;nn (8 bytes) vs m/km/k (6 bytes). You can't cache the first use "for free" because you'd need to wrap the assignment in parens. Oh it also eliminates the vx vy vars because we weren't using that value more often, anyways.

Commit 16; assignment squashing. Note that these are free since the value to store is not part of a bigger computation. Ironically, (much) later we'll see that this is actually bad for business due to repetitive code compressing better. But oh well.

Commit 17; just cleanup.

Commit 18; squash the switch into a double ternary. This switch is actually annoying but you'll want to eliminate it regardless and in a ternary golfable patterns may emerge that are not that obvious in a switch.

Commit 19; merge the double loop (for x y into a single loop that gets x y by computing it from the loop index. We can eliminate one of the loop counters as well.

Commit 20; some redundancy is more subtle. In this case the caching of p.x p.y was useless since it was used twice, the second time after being added and the result stored to itself. That means we can merge it easily ;)

Commit 21; assignment squashing. Does mean we need to take the root afterwards.

Commit 22; d was only used once. Eliminate it.

Commit 23; assignment squashing.

Commit 24; it looks like I'm undoing a previous step but we're actually chaining assignments together. That is "free" because it doesn't require parens as long as the expression on the left is already an assignment (a=b=c=d). The "compound" assignments are treated equally in that context.

Commit 25; and that opens us up for squashing the x = x - y to the compound assingment x -= y.

Commit 26; color name squashing. It also compressed the ugly conditional block to a single line ternary. Note that red is the shortest way to put that color in code, hex can not get that shorter. I'm still using it here, I think to find patterns and hopefully optimize that later in terms of repetitiveness.

Size is now 1427 bytes regpacked.

Commit 27; ahhh loop counters, who needs them. Loop squashing; instead of a loop counter assign the array contents in the loop conditional part of the header and as soon as the index exceeds the array size it will return undefined and exit the loop.

Commit 28; 1415; Statement squashing (turning a bunch of statements into a single "comma expression"). Doesn't work for all kinds of statements like loops and returns but it does for conditionals and, of course, "expression statements".

Commit 29; fake it till you make it... instead of a proper Euclidian distance it works well enough to speed up the particle with a fixed size and some random part. This way we can eliminate the complex and long square root. Result is very similar. The user will be none the wiser ;)

Commit 30; regression; the demo was a bit bugged at different viewports. The additions are a little sloppy and will be golfed down later.

Commit 31; simplify the "time" counter computations.

Commit 32; conditional squashing. And assignment chaining.

Commit 33; assignment squashing. Cheese it a little on the speed computations by always growing it rather than conditionally. Effect is the same..ish.

Size is now 1405 regpacked.

Commit 34; reorders vars and consolidates them so they can be eliminated easier. No real changes here. I think.

Commit 35; property name squashing. This will be undone later. If only I knew.

Commit 36; save a random() call by simply initializing speed to zero. Slightly changes the start of the demo but doesn't make it worse.

Commit 37; reverse the loop so that the loop header can be simplified.

Commit 38; renaming vars and props. Initialize a the pixel to 0,0 rather than center. This way the center value is used less often and that may help us eliminate stuff elsewhere.

Commit 39; the code was already doing random() * 0.5 so we can eliminate a use of random() / 2 because that's the same... :)

Commit 40; fake it till you make it; simplify accelleration code.

Commit 41; cache Math.PI. This is undone later and even simplified later later.

Commit 42; squash assignment, sort of.

Commit 43; formula simplification. I've fought long with this one because the two formula's look similar but are not identical. To revisit later.

Commit 44; golfing trick 301; adding dead code during the process means you get some free gains later on... ;)

Commit 45; eliminate unnecessary inverting. Sometimes it makes more sense in the code to invert some condition, but when golfing you may prefer to eliminate this, especially if the invert required parenthesis, because they can now disappear as well.

Commit 46; never do this. Instead of a === 0 ? x : a === 1 ? y : z we can also do [x,y,z][a].

Commit 47; since css colors have to be quoted in js you have two excess quotes for each color. This commit squashes those strings, with predictable length, together and splits them with a regex on four characters. This means '#c0c' can become x[1] and saves 2 bytes per color used.

Commit 48; after the css hack we can now optimize the array access a little.

Commit 49; assignment squashing, in fact, elimination. We only use the color once so instead just compute it with a ternary every time. Eliminates the property that stored it.

Size is now 1379 regpacked as is, 1286 without wrappers, vars, etc; "cruft". Still a long way to go.

Commit 50; dedupe string literals.

Commit 51; rename and move code, no real changes.

Commit 52; commit looks bigger than it really is (even without whitespace, it only inlines a function.

Commit 53; just one of those things. The canvas was cleared but other artifacts were already causing this. Free gains! :)

Commit 54; assignment squashing.

Commit 55; cache excessive p.u usages.

Commit 56; which we can of course do in the ternary, though it doesn't change the size right now.

Commit 57; tricky one: all sides of the nested ternary were assigning to the same variable, so instead assign the result of the entire structure once.

Commit 58; before each end point of the ternary was assigning to two vars but because that means having to wrap them in parens it was actually cheaper to make to individual ternaries.

Commit 59; cache a property access. Not sure what the && 4 was doing there.

Commit 60; assignment squashing.

Regpacked size now: 1376 / 1266 (dev / dist)

Commit 61; only moves code around.

Commit 62; resolve constant expressions.

Commit 63; assignment squashing. More like stuffing.

Commit 64; put magic numbers into constants to get a better sense of what's going on. Actually helps with golfing because at some point you forget what the numbers mean which may mean you're overlooking golfable patterns. And you'd miss out on awesome names like qfoxnlHeightWobbleViewportPart.

Commit 65; more of that.

Commit 66; calculus squashing: rewrite a * 0.2 to a / 5 and pull it inside. Eliminates the fraction and parenthesis.

Commit 67; the gloves are off. Remove the loop block because all the statements inside are expressions which can be chained as a long "comma expression".

Commit 68; one of those things that you'll overlook for a long time. Some variables don't have to start at zero. In this case, Z could not be too big or small but 600 was fine.

Commit 69; concessions... eliminating the constant 0.6 and replacing it by 0.5, which was already used a number of times. This means the text now bounced against the wall instead of near it but I guess I reached the point of becoming desperate :) That's when you start making concessions to get the code down.

Commit 70; calculus squashing. x/y = x*1/y and if y is a constant that means you can turn a fraction into an integer, especially if you can fake it a little, like in this case x * 0.07 = x / (1/0.07) = x / 14.2857.... However, 20 also works :p It still saves.

Commit 71; ohhh I really didn't like doing this. But I guess I was desperate enough to go for it. The commit changed the demo to rely on the js1k shim for centering it. That meant dropping a bunch of (relatively) expensive code that was centering the demo. Additionally I can now use relative coordinates and compute the origin from 0,0 rather than deriving it from the center coordinates. It takes a little to get over it but when you're stuck, you're stuck. I should clock my thinking times next demo ;)

Commit 72; property name cache jojoing.

Commit 73; unused var cleanup.

Commit 74; eliminate the var caching 0.5 and simply /2 everything.

Commit 75; curiously I was under the impression that drawing on half pixels was faster. I don't remember that this factoid changed but for whatever reason I dropped this optimization. Maybe there was another bug or it was overcompensating or my computer was overcompesating the perf loss or ... I dunno. I stripped it here and gained bytes. THAT's what counts.

Commit 76; swap stuff after using it once so that the result can be floored for the next use.

Commit 77; merges numbers that are close to each other where possible. In this case, 0.8 0.85 0.9 are all replaced with 0.88.

Commit 78; I'm using mod (%) to change the element so want to check when it's 0. This led to if (!(x%y)) ... But instead we can also do if (x%y < 1) ... and save a byte. Well, more with alternate versions but we can't really use the else here.

Regpacked size now: 1348/1241 (dev/dist). That was just 28 bytes? yo..

Let me skip a little to stick to the more interesting commits :) I think you can see the pattern of golfing emerge by now.

Commit 83; I was trying to be smart. After publishing the demo some golfers told me a much better method for doing this. But to heck with that, I tried to be smart by injecting parens into empty places in the element string. I was hoping to please the regpack gawds trying to trick them in consuming parts of the string as duplicate of actual code.

Commit 85; golfing 101: re-use a 0 parameter by using it as an initializer at the same time.

Commit 89; undo the merging of 0.8 0.85 0.9, it just wasn't worth it.

Commit 90; save a byte by doing binary or rather than logical or a|b instead of a||b. Ironically, it may actually be faster because conditional branching is actually quite expensive. Ok, that's irrelevant here.

Commit 93; you can merge two loops if they are of the same length and one is an initializer :)

Size now 1321 / 1215.

Commit 94; remember that mod trick? Let's improve on it: (Z++ % 150 > 0) ? 0 : ( to Z++ % 150 ? 0 : (. Doh!

Commit 95; fake it till you make it. Don't measure the font size, guess it! 'measureText' is such a long method name :(

And it goes on and on

Writing this post actually (already) took a lot of time and I'm not even half way the commits. I'm not very convinced it makes for an interesting post.

If you want to see more you can follow the commits step by step and the (sometimes cryptic) commit messages for comments. I don't know if this post clarified anything but it was an interesting experiment. But even reading it back myself I can't really get back into the mindset of "wow this was an awesome trick", while I've had some of those moments while creating the demo. Kind of disappointing. Kind of once again proves to me that writing a demo is often more fun than seeing one.

Oh well. At least now I can check this off my list :)