Size
Overview
- ROAR was generated from Goodluck.
- The key insight of Goodluck is that abstractions, generalizations, and parametrization are responsible for a lot of cruft in the code.
- Goodluck is a repo template rather than installable library.
- When you generate a new project from it, you get all the code in the Initial commit.
- It's yours to hack and modify.
- This approach allows a lot of customization that goes deep into the engine code.
- The final product that you ship with Goodluck is free of the burden of the code that it doesn't need.
- When you generate a new project from it, you get all the code in the Initial commit.
- Zip compression favors redundant code.
- Code that compresses well is often not what you'd call elegant.
- In order to help Zip compress the code:
- inline functions,
- avoid intermediate variables,
- repeat as many idioms throughout the codebase as possible.
- For this reason, I tend to stick to just a few flow control keywords (
if
,else
,switch
,for
) and I avoid more complex paradigms, like OOP's dynamic dispatch and FP's functions-as-first-class-objects approach.
Build
- ROAR benefits from the optimizations built into Goodluck.
- All code is bundled into a single file with Rollup.
- A few simple replacements are performed with
sed
. - Code is then minified using Terser.
- Terser's
--mangle-props
option shaves off over 1 KB in the final build. - Only properties whose names start with a capital letter are mangled.
- That's why you'll see a lot of
PascalCase
in ROAR and other Goodluck projects.
- That's why you'll see a lot of
- Terser's
- All assets (images, scripts) are inlined into the final HTML file with Posthtml, which uses the base64 encoding.
- Base64 typically has an overhead of around 30%.
- However, this is offset by the fact that without inlining, each additional file adds an overhead of around 100 bytes to the zip archive.
- The HTML file is then compressed with the 7-Zip CLI utility, using the most aggressive DEFLATE compression options.
- Compared to
gzip
, 7-Zip wins back around 200 bytes.
- Compared to
- Same as last year, I also maintained a release branch with some code removed.
- I removed some debugging code.
- I removed WebGL error checks.
- I replaced
throws
withreturns
.
- Finally, I used
advzip
to optimize the zip archive even further.- It saved around 130 bytes.
Textures
- Textures in ROAR are 8x8 or 16x16 pixels in size.
- Initially, I used PNGs exported from the editor I used to draw them, PyxelEdit.
- There are 4 textures for buildings. They are ~150 bytes on average each.
- I then heard about lossless WebP on js13kGames Slack.
- I used the
cwebp
CLI encoder. - The
-z 9
option turns off the best lossless encoding. - With WebP, the building textures are ~100 bytes on average.
- I used the
- I created a
Makefile
to handle the encoding from PNG to WebP. - I also experimented with optimizing textures further using multichannel textures.
- Conveniently, all my art uses a simple palette of 8 colors.
- The idea was to convert all assets to grayscale by assigning each colors from the palette to a shade of gray.
- It would then become possible to encode 4 grayscale textures in the 4 channels of an RGBA texture.
- ImageMagick has a dedicated
-combine
option for this.
- ImageMagick has a dedicated
- I then planned to define the palette in the shader, and pick the right fragment color based on the shade of gray in one of texture's channels (passed as an extra uniform).
- I created a prototype of this approach on a branch.
- Unfortunately, I ran into problems with pre-multipled alpha, and I wasn't able to figure out exactly how to turn it off.
- With pre-multiplied alpha, the RGB channels are multiplied by the value of the alpha channel. This isn't desired when each channel is supposed to encode a different image, of course.
- Assuming I could figure the alpha issues out, I was still surprised by the fact that this approach only saved 20 or 30 bytes compared to the naive one!
- Because they contain much more information, the combined textures compress much worse than the separate ones.
- 4 separate building textures are 405 bytes.
- 1 combined buildings texture is 234 bytes.
- 4 separate props textures are 226 bytes.
- 1 combined props texture is 124 bytes.
- Even though the multichannel approach replaces 8 textures with just 2, the savings in bytes are less impressive.
- 8 separate textures are 630 bytes
- 2 combined textures are 358 bytes
- That's a saving of 272 bytes.
- 4 separate building textures are 405 bytes.
- The savings from the multichannel approach were offset by the fact that I now needed to define the color palette in the shader.
- Passing the extra uniform to the shader and parameterizing the render components added some code, too.
- Because they contain much more information, the combined textures compress much worse than the separate ones.
- Unfortunately, I ran into problems with pre-multipled alpha, and I wasn't able to figure out exactly how to turn it off.
- In the end, I decided not to pursue this approach.
- The net savings were small.
- It increased the complexity of the code and the build system.
- I didn't have the time to figure out the issues with pre-multiplied alpha.
Statistics
- ROAR has around 100 files and 6,500 lines of TypeScript code.
- The single JS file bundled with Rollup is 158,891 bytes.
- It includes all whitespace, blank lines and comments.
- With blank lines and indentation stripped, the code is 97,937 bytes.
- When minified with Terser, it's down to 37,926 bytes.
- The final HTML file with all resources inlined is 39,760 bytes.
- All the textures take only a total of 1,387 bytes, including the
img
tags.
- All the textures take only a total of 1,387 bytes, including the
- The Zip archive compressed with 7-Zip is 13,303 bytes.
- That's already under the 13 KB limit, which is exactly 13,312 bytes.
- The final Zip archive after
advzip -z4 -i5000
is 13,186 bytes.