In the js13kgames competition, you have 30 days to produce a game that can be bundled into a ZIP file of no more than 13,312 bytes. If you find yourself frequently running into this limit, here’s a collection of tips and tricks that might help.
This is the obvious one, and usually the first thing to tackle.
- For HTML, htmlmin
- For CSS, clean-css
- For images, imagemin
If you’re using a gulp-based build system, you can use the gulp wrappers for all of these. A basic
that includes the tools above might look something like this:
index.html. A simple, popular way
to do this is to insert a
one of the packers, like webpack, rollup, or
parcel, but be conscious of shims and boilerplate code - see suggestion #7 below.
With the naked eye, it can be hard to pick out variables or properties you ended up not using. Consider
gulp-eslint to your project to help flush those things out.
If you want to focus just on saving space, you only need to turn on a few rules in your eslintrc:
For spotting code you ended up not needing, turn on
For spotting code you could shorten, try
Don’t bother with
prefer-template, as terser is smart enough to rewrite all your strings to make them as small as possible (including eliminating unnecessary string concatenation).
no-var. More generally, you should pick only one of
const, and use it exclusively: I recommend
let. This will save you exactly 8 bytes in your zip file, since the keywords
varwill never exist in the output source code.
Tip: It’s worth taking a second to consider the nature of a ZIP file itself, which is basically
a collection of files compressed with Huffman encoding. What this means is that everything in your
zip file is turned into a dictionary of commonly used phrases, and your files are stored as references
to those dictionary entries, with the most commonly used entries organized to take the least amount
of bits to reference.
Optimizing for space has a certain “all or nothing” aspect to it. If your code has 500 calls to
game.updatePlayer(), then these references will be optimized
during compression to take up next to no space: reducing the 500 calls to 5 calls may save you next
to nothing, or even increase the size of your zip file! However, reducing it to 0 calls guarantees
that you will lower the total size by at least the number of bytes it takes to store a single copy
of the string.
This is why mangling is so powerful, because it removes all copies of the original names - see suggestions #6 and #8 below.
Feel free to turn on whatever other eslint rules you like based on your own preferences. The ones above are the most likely to save you space, but other rules can still be helpful in keeping your code clean and/or finding potential bugs.
As far as bang for the buck goes, if you have any images at all, this is a big one.
A “sprite sheet”, if you’ve never seen the term, is just a bunch of smaller images combined into a single image file, usually in a grid. (Although if you’re willing to keep track of the locations of each image, you can pack sprites of different sizes into a sprite sheet as tight as you can get them.)
Even with just a simple grid, though, the savings can be huge. For a concrete example, check out these 3 simple sprites for an imaginary game involving missiles and frogs:
Just by combining our images into a single image, we cut almost 40% of our total asset size. (The savings can get much higher than that, especially if you have several similar-looking sprites, such as variations of level tiles.)
For small games like the ones you are likely to build for the js13k competition, setting
up a sprite sheet by hand should be pretty straightforward. How you use the sprites will
depend on your graphics (a CSS-based game can use standard CSS sprite techniques, while
a Canvas-based game will use the long version of
drawImage, which lets you specify the
height of the source sprite within your sprite sheet).
Due to the way PNGs are compressed, images with long blocks or runs of identical color will produce much smaller file sizes. Try to pick a specific palette and use those colors wherever you can. Avoid machine-created gradients (which use lots of colors). Where possible, avoid speckling and noise.
Try doing a google image search for
cel shading to see some examples of art that uses very
few colors (and large blocks of color). Hopefully, you are a better artist than me, because
I have very little advice to give you here!
This is very dependent on what look you’re going for and what assets you need. Just be aware that often, the code to draw a simple crosshair on the screen is probably smaller than a similar PNG of a crosshair; the code to draw a 64x64 smooth gradient circle will certainly be many times smaller than a PNG image of the same shape.
In some cases a creative hybrid approach may work. For example, you could use very simple PNG images as a building block, and then overlay some noise on the image to give the desired effect. You could have a single PNG template, which you then recolor or skew or overlay to give additional desired image frames you need in your game.
Be aware that drawing a very complex object (like a player character) using a bunch of basic
primitives might end up much slower than a single
drawImage. Do your own testing,
but in some cases, it may be better to deal with the space cost of the PNG than the
performance cost of drawing with primitives.
By the time your game is complete, your code is full of more or less expressive property
and method names - code like
player.x += player.getNewVelocityX(game.deltaTime) contains
a bunch of stuff that by default is not mangled (shortened) by terser.
Most local variables and function arguments and such are already going to be mangled, which
leaves just property names. If you see a lot of readable English – like
playWeirdTrainNoise – in your minified output, that’s a sign
that property mangling could help shrink your zip file size.
The problem with property mangling is that if you mangle all of them, your game will break.
Inevitably, you need to call APIs outside of your control (functions like
getElementById are all examples), if you mangle these names, you’ll
be attempting to call functions that don’t exist.
I recommend, by default, mangling only properties starting with
_. This is convenient because
_name to indicate the property
or method is a private property and shouldn’t be relied upon to exist. If most of your classes
and objects refer to their internal states (like
this._accelerationX) with underscored names,
which lets you know they are safe to mangle. You can enable this in your gulpfile like so:
You can put pretty much anything in that regex, so if you use certain method names a lot and don’t
want underscores everywhere in your code, feel free to add them in to mark them OK to mangle: for example,
UPDATE 02/24/2019: I now prefer mangling with the reserved option, see suggestion #8 below.
and see how much (if any) boilerplate there is. Most packing utilities and even transpilers end up
including shims, or
import implementations, as boilerplate headers, and in some cases this
can add up to 100s of post-minification bytes.
When you need to wring maximum space out of your code, one way to do it is to abandon
marking properties as “safe to mangle”, and instead mark properties that aren’t safe to
mangle. The properties that aren’t safe are the ones mentioned last time: APIs you use on
on your Canvas context or AudioContext, etc.
Note that terser will already avoid mangling functions on many of the core objects, like Array,
Math, Date, Function, etc. So you don’t need to go crazy and list every
forEach you use,
those will automatically be left alone.
If you are going this route, you should probably just turn on toplevel mangling as well, to mangle your root-level class names and object names (if any).
Example of what this might look like in your gulpfile:
If you plan on going this route, it might help to start early - this way you can add to the list of reserved property names as you write new code. If your project is already finished, prepare for a long night of debugging while you get it set up! The result, though, will be a minifed product that can’t get much smaller.
Note that terser does not care what object a property or method lives on; it cannot distinguish between,
enemy.update(). However, once it assigns a property
a new mangled value, it will use it throughout your entire codebase, so it also doesn’t need to care -
all references to
update on all objects will be mangled to a new, shorter name.
Because terser won’t mangle properties available on the common APIs of things like String, Math, and others,
make sure not to reuse names like
length, etc. for your own properties. Your code will work
fine, but those properties won’t get mangled! Pick different names, or use the underscore trick from
UPDATE 02/24/2019: Terser is now quite good at reserving all known browser API methods by default, making this suggestion much easier. I now like this approach the most (just make sure you use the latest available version of terser).
AdvanceCOMP project, available from AdvanceMAME, gives two
useful utilities for further compressing your images and ZIP files. If you’re on Mac or
Linux, you’ll need to build from source, which can sometimes be a project all of its own (although
my experience was that this was pretty easy on MacOS). This tool is also listed on the js13k resources page.
advpng tool allows you to recompress PNGs. My experience is that for most images, it cannot
get smaller (and even when it can, it’s only a handful of bytes), but it never hurts to try.
advzip tool is a little more impressive, I found it could shave another 3-5% off of the final zip
file size - late in the competition, that’s 300-400 bytes of additional code, which can be quite a
bit of final polish.
Both tools also offer an option to spend even more cycles finding the smallest possible encoding;
again, this is going to squeeze out only a small handful of bytes, and it boosts the time it
takes from 2-3 seconds to 60+ seconds, but if you really want the smallest possible output, use
One way to integrate
advzip into your build pipeline is by using the
I consider this a “last resort”, but I did say we were making the smallest possible ZIP file here…
Keeping in mind the way ZIP file compression works, one way to eliminate extra bytes
is to ensure you are calling the fewest possible core functions. For example: if you use
.match() is typically able to do the job of either. If
you use both
.concat([value]) can be used instead of
.push(). If you use both
forEach(), and don’t have any loops that you break
out of, you might be able to replace all
forEachs. Ditto for
At this point, in my opinion, you’re beginning to damage your source code‘s legibility, so how far you want to take it is up to you. My recommendation would be to stop short of this step and consider cutting a feature, shortening a music loop, simplifying a sprite, dropping a level, doing anything other than butchering your own poor, defenseless source code…
Have you discovered a trick for shaving a few bytes off your ZIP file? Feel free to post a comment below.