pouët.net

Go to bottom

State of the art tooling for 4k intro for the Web

category: code [glöplog]
 
Hi, we do have a handful of awesome 4k intros for the Web by HBC/½-bit Cheese from a few years ago, and more recently by Bit Labs. But by looking at this content as well as that produced by the JS1k/JS13k scenes, it seems size coded JS/Web demos suffer from harder size limitations than our Windows native ones. So I've looked into the latest tools and tricks for size coding on the Web (thanks p01, 0b5vr and https://in4k.github.io/wiki/javascript), and tried to quantify how much is actually lost in bytes when going from Windows to the Web.

To do so, I’ve rewritten Elevated in JS for the Web and compared its size to the original Windows version with all I’ve learnt.

Because I’m intimately familiar with this piece, I was able to not simply “ported” it but do an actual apples-to-apples comparison, where I've re-written parts of the intro to better compress for the Web. I pretended to be working towards a JS release for a demo party, trying to make the damn thing fit in 4096 bytes in all ways possible. When keeping every polygon, pixels and sound-sample exactly as in the original, the intro turned out to be a 5.5 kilobyte self-contained HTML. When I relaxed the fidelity constraints a little, I was able to recreate what’s basically the same intro in 5.3 kilobytes. In both cases I sacrificed a bit of code to respect window resizing, first user interaction before sound, etc.

So my conclusion so far is that the penalty for bringing 4k intros to the Web in JS is about 1.33x, i.e., a 4k JS intro for the Web is approx equivalent to a 3 kb Windows intro.

This is of course only data point (one intro, one coder).

Now the more useful information that hopefully others can critizice and improve upon is:

  • I explored multiple ways to encode the data (script, soundtrack and minified shaders) through multiple layouts and prediction strategies, as well as encodings as plain arrays, Base64 and UTF-16.

  • I tried multiple bundlers and code minifiers+manglers+rewriters+AST optimizers, including Esbuild, Terser and Closure, all three with and without Roadroller.

  • I tried multiple ways to pack the intro, including RegPack, PNG bootstapping, Sagacity's recent Mashi (intended for 64k intros, not 4k), and a custom thing I’ve built with Zopfli/DEFLATE and a boostrapper (similar to 0b5vr’s Compeko and Sagacity's Mashi).

  • I also developed some custom scripts to hash WebGL function calls and replace GL literals by numerical values, which does help.


I tried most of the different permutations of the packers and the bundler/minifiers above, but in the end this was the winning worfklow: shader minifacation -> WebGL hashing and literal rewrite -> bundle with Esbuild (not minimize) -> minimize and mangle with Terser -> pre-compress with Roadroller -> compress with Zopfli -> append it to an HTML file with the <svg> stub trick. Many of you might know this workflow already, or maybe you have alternate suggestions? Please share.

Now these are the things I’ve learnt, maybe the align with your experience too?

  • A Web intro needs more boilerplate than a native one. A native intro can open a window, go fullscreen and listen to a key to exit in very few bytes. But a JS intro needs a lot to properly respond to the changing window sizes or going fullscreen, listening to the users first interaction to unlock the sound context creation, and doing the minimally necessary CSS.

  • Besides boilerplate, while the core JS code minifies and compresses okay with Zopfli/DEFLATE, it’s hard to beat Crinkler on x86 really.

  • WebGL is way too verbose and there's only so much that function hashing and replacing literals can do. WebGPU is even worse, about 300 bytes larger. Now, Elevated touches a large surface area of the WebGL API because it’s a traditional mesh renderer that creates textures, FBOs, depth buffers, etc. Shader-only intros that only touch a handful of API points might be a much better fit for JS/the Web.

  • Still, the biggest difficulty for me was storing the large amounts of binary data in this intro. I got some success de-interlazing some of the script data channels and doing more efficient encoding than in the original (funny, Elevated could have been a 3.8k intro probably). But no matter the technique, storing whatever data you have in the end is tricky. Neither Base64 nor UTF-16 actually was able to beat plain text after the DEFLATE pass. Data storage is the main reason my JS version of Elevated is not a 4k intro.


So in the end, these are my take-aways:

  • Unless there’s a simple solution to the data problem that I’ve missed, it seems that a 4k intro for the Web can be expected to look more or less like a 3k intro for Windows.

  • As of today, intros that are “shader only” (no meshes) and contain less data than Elevated (probably using more procedural yet art-directable scripting and soundtrack) are a better fit for the Web.

  • It might be worth exploring storing the bulk of the intro in WASM (appended to the final HTML), since WASM can encode the binary data as actual blobs. I've done early experiments with writing the intro in C with Emscripten that look promising, but I'll report fully when I complete this experiment.

  • There are a lot of tools out there available (and I haven’t even touched on shader minimizers), with varying degrees of effectiveness but all claiming being the best for extreme size coding (1kb to 4kb), making learning confusing. I think there's an opportunity for a tool that unifies the current workflow that you otherwise have to ducktaped, that is dedicated for 4k intros like Crinkler.


If you’ve done experiments of your own or have better ideas and methods, please let me know. Thanks for reading!
added on the 2026-09-24 01:22:47 by iq iq

login

Go to top