Skip to main content

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.wasmMB
raw17.1
gzip6.3
brotli4.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 dependencyRawBrotli
egui's default fonts: Hack, Ubuntu Light, Noto Emoji, emoji-icon1.4 MB0.7 MB
kiss3d's tonemapping lookup table, tony_mc_mapface.bin0.9 MB0.2 MB
kiss3d's debug-text font, Work Sans0.1 MB0.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.

FeatureDefaultWeb templateWhat it isAdds to a web build
appleoffoffapple.* 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
audioononSound. Off, a build drops rodio, cpal and the platform audio stack.cpal, rodio, symphonia, balaur_audio, chacha20, dasp_sample, … (23 crates)
extensionsoffoffLoad extensions from a project's extensions/ directory at run time.libloading
gamendonongamend.* for scripts: the Gamend backend (auth, REST, realtime, hooks).balaur_gamend
httpononhttp.* for scripts. Off, a build drops ureq and its TLS stack.balaur_http
webononweb.* for scripts: the page a browser build runs in. Always compiles; off the web every call answers nil.balaur_web
websocketononwebsocket.* for scripts, and the websocket Transport. Off, a build drops tungstenite, rustls and the frame codec.balaur_websocket
webtransportonoffThe 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
windowoffonWindowed 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).

CrateVersionFeatures on
egui0.36.1default_fonts
wgpu30.0.1dx12, fragile-send-sync-non-atomic-wasm, gles, metal, parking_lot, std, vulkan, web, web-sys, webgl, webgpu, wgpu-core, wgsl
image0.25.10avif, bmp, dds, default-formats, exr, ff, gif, hdr, ico, jpeg, png, pnm, qoi, tga, tiff, webp
rodio0.22.2cpal, 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
rapier3d0.35.3alloc, debug-render, dim3, enhanced-determinism, f32, parallel, serde-serialize, std
parry3d0.30.2alloc, dim3, downcast-rs, ena, enhanced-determinism, f32, hashbrown, indexmap, parallel, rayon, required-features, rstar, serde, serde-serialize, serde_arrays, slab, smallvec, spade, std
cosmic-text0.19.0shape-run-cache, std, swash, sys-locale
wesl0.4.4eval

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. egui is built with default_fonts on, by balaur_ui and by kiss3d, and the UI plugin falls back to them when a project ships no fonts/*.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 on wgpu/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 image has, 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_physics links rapier2d and rapier3d both, so a 2D game carries the 3D solver and parry3d. The crate is written 3D-first with dim2 as a module, which makes a split a refactor rather than a flag. parry's parallel feature 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 harfrust 0.12; cosmic-text, which balaur_ui uses for marked-up text, with harfrust 0.5. Two copies of the shaping tables, and of read-fonts and skrifa, 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.