Build size
A web game is a download before it is a game, and the engine module is most of that download. This is the module behind the editor and the playable examples on this site, measured on every deploy:
balaur_bg.wasm | MB |
|---|---|
| raw | 17.1 |
| gzip | 6.3 |
| brotli | 4.6 |
Which number a player waits for depends on the host: brotli when it
compresses .wasm (check the content-encoding header it sends), gzip when
it only does that, raw when it does neither. The raw size is what the browser
compiles either way. The pack is on top, and it is the game's own bytes.
Where the bytes go
One module carries the whole engine, and nothing in it loads lazily: the renderer (wgpu with both its WebGPU and WebGL2 backends, naga, winit), the UI (egui and its four default fonts, plus a second text shaper for marked-up text), physics in 2D and in 3D (rapier and parry, both dimensions), the Rune compiler and VM, every audio codec rodio turns on, and every image format kiss3d turns on.
Code is about three quarters of the module. The rest is data, and half the data is assets that dependencies bake in:
| Baked in by a dependency | Raw | Brotli |
|---|---|---|
| egui's default fonts: Hack, Ubuntu Light, Noto Emoji, emoji-icon | 1.4 MB | 0.7 MB |
kiss3d's tonemapping lookup table, tony_mc_mapface.bin | 0.9 MB | 0.2 MB |
| kiss3d's debug-text font, Work Sans | 0.1 MB | 0.05 MB |
The other half is tables: Unicode data for two text shapers, codec tables, shader sources. The figures are from the nightly of 2026-09-05 and move with the dependency versions.
Building a smaller template
balaur export --target web copies a template, the glue and module that
scripts/package_template.sh web in the engine repository builds, and puts
the pack and a page beside them. Build a template with fewer features and
hand it to the exporter:
WEB_FEATURES=window,web scripts/package_template.sh web
balaur export my-game --target web --template dist/balaur-template-web
The script wants the wasm32-unknown-unknown target, the wasm-bindgen
CLI at the version in Cargo.lock (it checks), wasm-opt from binaryen
and, for the size line it prints, brotli. It refuses a module under 5 MB,
which is what a build whose entry point was optimised away looks like.
On a browser build only two features weigh anything: audio and window,
and a game without a window is not a game. Everything else keeps its native
stack off the wasm target and costs the plugin's own code.
Features
The cargo features of balaur_cli, the binary every runtime template is built
from, and what each adds to a wasm32-unknown-unknown build. A feature's native
dependencies are gated off that target, so http or websocket costs a browser
build only the plugin's own code; the two that matter there are audio and
window.
The web template (scripts/package_template.sh web) is built with
--no-default-features --features audio,http,websocket,gamend,web,window and links 373 crates.
Override the set with WEB_FEATURES=... scripts/package_template.sh web.
| Feature | Default | Web template | What it is | Adds to a web build |
|---|---|---|---|---|
apple | off | off | apple.* for scripts, and Game Center and iCloud under platform.*. Off, a build drops GameKit and the objc2 bindings to it. platform.* is always there; with no store behind it every call answers unsupported. | balaur_apple |
audio | on | on | Sound. Off, a build drops rodio, cpal and the platform audio stack. | cpal, rodio, symphonia, balaur_audio, chacha20, dasp_sample, … (23 crates) |
extensions | off | off | Load extensions from a project's extensions/ directory at run time. | libloading |
gamend | on | on | gamend.* for scripts: the Gamend backend (auth, REST, realtime, hooks). | balaur_gamend |
http | on | on | http.* for scripts. Off, a build drops ureq and its TLS stack. | balaur_http |
web | on | on | web.* for scripts: the page a browser build runs in. Always compiles; off the web every call answers nil. | balaur_web |
websocket | on | on | websocket.* for scripts, and the websocket Transport. Off, a build drops tungstenite, rustls and the frame codec. | balaur_websocket |
webtransport | on | off | The WebTransport Transport, over QUIC: the transport rollback and replication are meant to run on. Off, a build drops quinn, its runtime and the certificate machinery, which is most of what a networked build costs. | balaur_webtransport |
window | off | on | Windowed rendering (kiss3d/wgpu). | egui-wgpu, exr, glow, kiss3d, naga, wgpu, … (70 crates) |
What the web template resolves
The features on in the dependencies that weigh most. A feature named here is
enabled; whether it links code on the web is up to that crate's own target
gates (winit's X11 is on and compiles nothing in a browser).
| Crate | Version | Features on |
|---|---|---|
egui | 0.36.1 | default_fonts |
wgpu | 30.0.1 | dx12, fragile-send-sync-non-atomic-wasm, gles, metal, parking_lot, std, vulkan, web, web-sys, webgl, webgpu, wgpu-core, wgsl |
image | 0.25.10 | avif, bmp, dds, default-formats, exr, ff, gif, hdr, ico, jpeg, png, pnm, qoi, tga, tiff, webp |
rodio | 0.22.2 | cpal, dither, flac, mp3, mp4, noise, playback, rand, rand_distr, recording, symphonia, symphonia-aac, symphonia-flac, symphonia-isomp4, symphonia-mp3, symphonia-ogg, symphonia-pcm, symphonia-vorbis, symphonia-wav, vorbis, wasm-bindgen, wav |
rapier3d | 0.35.3 | alloc, debug-render, dim3, enhanced-determinism, f32, parallel, serde-serialize, std |
parry3d | 0.30.2 | alloc, dim3, downcast-rs, ena, enhanced-determinism, f32, hashbrown, indexmap, parallel, rayon, required-features, rstar, serde, serde-serialize, serde_arrays, slab, smallvec, spade, std |
cosmic-text | 0.19.0 | shape-run-cache, std, swash, sys-locale |
wesl | 0.4.4 | eval |
What is not a feature yet
The levers below are dependency lines in the engine, not switches. Each is named so a fork can flip it, with a size where one has been measured.
- egui's default fonts.
eguiis built withdefault_fontson, bybalaur_uiand by kiss3d, and the UI plugin falls back to them when a project ships nofonts/*.ttf. A game that ships its own fonts carries both sets: 1.4 MB raw, 0.7 MB brotli, for nothing. - The WebGL2 fallback.
egui-wgpu's default features turn onwgpu/webgl, which links the GLES backend (glow) and naga's GLSL writer beside WebGPU. Current browsers ship WebGPU; the fallback is for older ones. Unmeasured. - Image formats. kiss3d enables every format
imagehas, so the module links decoders for EXR, TIFF, GIF, QOI and more that no pack uses: a pack's textures are PNG, JPEG or WebP. Unmeasured; EXR alone is a large decoder. - Audio codecs. rodio's defaults are FLAC, MP3, MP4/AAC, Vorbis and WAV, plus dithering noise and a recording path. One feature per codec would let a game that ships Ogg Vorbis drop the rest. Unmeasured.
- Physics in one dimension.
balaur_physicslinks rapier2d and rapier3d both, so a 2D game carries the 3D solver and parry3d. The crate is written 3D-first withdim2as a module, which makes a split a refactor rather than a flag. parry'sparallelfeature is on too, and rayon has no threads to use in a browser. - The tonemapping table. 0.9 MB raw, 0.2 MB brotli, for HDR tonemapping in kiss3d. An analytic curve costs no bytes.
- Two text shapers. epaint shapes with
harfrust0.12;cosmic-text, whichbalaur_uiuses for marked-up text, withharfrust0.5. Two copies of the shaping tables, and ofread-fontsandskrifa, are linked until the two agree on a version.
What is already done leaves no knob on stable Rust: the web profile in the
engine's Cargo.toml sets opt-level = "z", fat LTO, one codegen unit,
panic = "abort" and stripped symbols, and the script runs wasm-opt -Oz
after bindgen. A -Zbuild-std with panic_immediate_abort would take a
further slice and needs nightly, which the pinned toolchain rules out.