Build size: what a web build carries
What is in the Balaur web module, which cargo features and dependencies decide its size, and how to build a smaller runtime template.
On this page
The engine module is most of a web game's download. Two modules are built
from one crate: the game template balaur export --target web ships, and the
editor's module behind the editor and the examples.
balaur_bg.wasm, 2026-10-04 |
Raw | Brotli |
|---|---|---|
| Game template | 18.9 MB | 5.1 MB |
Game template, physics2d only |
17.1 MB | 4.7 MB |
Game template, physics3d only |
17.5 MB | 4.8 MB |
| The editor's module | 25.4 MB | 7.0 MB |
- The host decides which number a player downloads: brotli when it compresses
.wasm(see thecontent-encodingheader), gzip when it only does that, raw when it does neither. - The browser compiles the raw size either way.
- The pack, the game's own bytes, is on top.
What a game template leaves out
The template is balaur_cli built without four features the editor's module
has.
editor: the exporter and its encoders (FLAC, a resampler, a palette quantiser, zip), the project store and a tab's editor entry points. 0.4 MB of code.compile: Rune's compiler, parser and formatter, the standard library's macros that reach them, and the WESL shader linker. 1.0 MB and 0.9 MB of code. An export ships compiled scripts, and every shader its game draws with linked and deflated into the pack: 9 KB for a game with no materials of its own. A pack exported with--keep-sourcesruns only in a build withcompile.fallback-fonts: egui's Hack, Ubuntu Light and two emoji faces. 1.4 MB of data. A game draws with the faces under its ownfonts/and the Source Sans 3 face the engine carries.import: the importers and SVGs read in place.
Every build decodes images as PNG, JPEG, WebP, BMP, TGA, HDR and EXR, the
formats a material and a sky read. The marked-up text shaper and egui's share
one harfrust and one read-fonts.
Module contents
One module carries the whole engine; nothing loads lazily.
| section | Editor's module | Game template |
|---|---|---|
| code | 20.0 MB | 15.6 MB |
| data | 6.3 MB | 4.1 MB |
Code by crate, from the same builds with symbols kept. Each function is charged to its defining crate; 1% of the code names no crate.
| MB | Editor's module | Game template | Without physics3d |
|---|---|---|---|
| the engine's own crates | 4.66 | 4.00 | 3.62 |
| Rune | 2.20 | 1.22 | 1.22 |
| the renderer: wgpu, naga, winit, kiss3d | 1.55 | 1.55 | 1.55 |
| text: egui, epaint, shaping, fonts | 1.50 | 1.50 | 1.50 |
| physics in 3D: rapier3d, parry3d | 1.13 | 1.12 | 0.18 |
| the WESL linker | 0.94 | 0 | 0 |
| physics in 2D: rapier2d, parry2d | 0.83 | 0.84 | 0.84 |
| image codecs | 0.63 | 0.50 | 0.50 |
| audio codecs: symphonia, rodio, cpal | 0.44 | 0.44 | 0.44 |
| the exporter's encoders | 0.30 | 0 | 0 |
The rest is core, alloc and the serialisation crates. egui fills its
glyphs with zeno, the rasteriser the engine's own text uses, through a patch
on epaint 0.36. scripts/wasm_crates.py in the engine repository prints
the per-crate list behind this table from a module built with its symbols kept.
| Baked in by a dependency | Raw | Brotli |
|---|---|---|
kiss3d's tonemapping lookup table, tony_mc_mapface.bin |
0.9 MB | 0.2 MB |
| egui's default fonts, the editor's module only | 1.4 MB | 0.7 MB |
The rest of the data is tables: Unicode data for the text shapers, codec tables, and kiss3d's built-in shaders, linked when it compiles.
Pack size
The pack is the second download. Every export prints a report.
pack 2.0 MB
section entries bytes
manifest 1 78 B
scenes 1 3.4 KB
scripts 37 488.2 KB
assets 6 1.6 MB
extension entries bytes
ttf 4 1.5 MB
png 2 22.7 KB
largest bytes
fonts/icons-Phosphor.ttf 477.2 KB
fonts/ui-SourceSans3-Regular.ttf 421.1 KB
fonts/heading-SourceSans3-Semibold.ttf 415.9 KB
assets/balaur-logo.png 20.2 KB
0 files nothing references, 0 B
- Fonts outweigh images in most projects, and the engine outweighs both.
- On the engine's own assets, subsetting takes a 421 KB face to 29 KB and the image pass takes a small sprite from 1.1 KB to 0.5 KB.
- Four
[export]keys act on the report, each off by default.

[export]
strip = true # drop an asset no scene, script or include glob names
include = ["sfx/**"] # ...except paths a script builds at run time
image_recode = "webp" # write lossless WebP
font_recode = "subset" # cut a face to the characters the project shows
font_original = ["fonts/ui-*"] # ...but ship these whole
audio_recode = "flac" # write uncompressed WAV as lossless FLAC
Anything under fonts/ survives a strip: the UI finds faces by listing that
directory, and no scene names one.
Two more keys are lossy, and named separately from the lossless ones.
[export]
image_recode = "quantized" # a 256-colour palette with alpha, then the PNG pass
image_quality = 80 # the palette's quality target, 0 to 100
audio_recode = "vorbis" # an Ogg Vorbis stream instead of the original PCM
audio_quality = 0.5 # libvorbis quality, -0.1 to 1.0
One picture overrides the mode in its own import settings.
# art/logo.png.import.toml
recode = "original" # this one ships exactly as drawn
- Every mode except
quantizedandvorbisis lossless. None changes a pixel count. - A sprite is sized from its image header; a re-encoded texture is the same sprite.
balaur export --dry-runprints the table and writes nothing.- An atlas is not a size lever: it saves each file's header and little else. The renderer draws one call per node either way.
- GPU-compressed textures (BC, ETC2, ASTC) cut video memory and upload time, not the download. A block-compressed image on disk is larger than the PNG it came from.
Building a smaller runtime
Every release carries a template per physics world for every target, which the project picks (see Shipping).
[export]
runtime = "2d" # web-2d: no rapier3d; "3d" leaves out rapier2d
A template built with fewer features still is passed to the exporter.
WEB_FEATURES=audio,vorbis,browser,window,physics2d scripts/package_runtime.sh web
balaur export my-game --target web --runtime dist/balaur-runtime-web
physics2dandphysics3deach register one world. Withoutphysics3da template is 1.8 MB smaller raw and 0.4 MB brotli; withoutphysics2d, 1.4 MB and 0.3 MB.- A world left off still compiles; its systems, components and verbs are never registered, so its solver never links.
flac,mp3,mp4,vorbisandwaveach decode one audio format. MP4 and AAC is the largest at 0.14 MB; all five are 0.32 MB.[export] audio_recode = "vorbis"rewrites WAV as Ogg Vorbis, so a project of WAV and Ogg files plays on avorbisbuild.- The script needs the
wasm32-unknown-unknowntarget, thewasm-bindgenCLI at the version inCargo.lock(checked),wasm-optfrom binaryen, andbrotlifor the size line it prints. - It refuses a module under 5 MB: a build whose entry point was optimised away.
http,websocket,webtransport,gamendandmultiplayergate their native stacks off the wasm target and cost the plugin's own code.
Features
The cargo features of balaur_cli, the binary every runtime 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 runtime (scripts/package_runtime.sh web) is built with
--no-default-features --features audio,flac,mp3,mp4,vorbis,wav,http,websocket,webtransport,gamend,multiplayer,browser,physics2d,physics3d,window and links 303 crates.
Override the set with WEB_FEATURES=... scripts/package_runtime.sh web.
WEB_THREADS=1 builds the second runtime, which adds parallel to that
set: rapier's solver threads on rayon, which needs the shared memory and
atomics only that build has.
| Feature | Default | Web runtime | 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, atomic_float, balaur_audio, chacha20, … (24 crates) |
browser |
on | on | browser.* for scripts: the page a browser build runs in. Always compiles; off the web every call answers nil. |
balaur_browser |
compile |
on | off | The Rune compiler and the WESL shader linker: a dev run, a pack carrying sources, the editor and balaur check. A template leaves it off and draws with the shaders an export linked into the pack. |
nothing |
editor |
on | off | The editor: edit and export, the export.* and project.* modules its scripts call, a browser tab's editor entry points, and egui's bundled faces. A game template leaves it off. |
audio-codec-algorithms, audioadapter, audioadapter-buffers, audioadapter-sample, balaur_export, crc, … (25 crates) |
extensions |
off | off | Load extensions from a project's extensions/ directory at run time. | libloading |
flac |
on | on | One feature per container audio decodes; a build without one plays none of that format. |
symphonia-bundle-flac |
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 |
import |
on | off | balaur import, import.* for the editor and SVGs read in place. The web game runtime leaves it off: a game reads what an import and export wrote. |
arrayref, aseprite-loader, balaur_import, color_quant, data-url, float-cmp, … (30 crates) |
mp3 |
on | on | symphonia-bundle-mp3 |
|
mp4 |
on | on | symphonia-codec-aac, symphonia-format-isomp4 |
|
multiplayer |
on | on | multiplayer.* for scripts: host, join and play a rollback match over whichever of websocket and webtransport the build has. |
balaur_multiplayer |
parallel |
on | off | rapier's solver on rayon. Native builds want it; a browser can only take it with shared memory and atomics, which is the threaded web runtime alone. | crossbeam-deque, crossbeam-epoch, crossbeam-utils, rayon, rayon-core |
physics2d |
on | on | The 2D physics world: physics2d.*, its components and presets. |
nothing |
physics3d |
on | on | The 3D physics world: physics3d.*, its components and presets. |
nothing |
vorbis |
on | on | symphonia-codec-vorbis, symphonia-format-ogg |
|
wav |
on | on | extended, symphonia-codec-pcm, symphonia-format-riff |
|
websocket |
on | on | websocket.* for scripts, and the websocket Transport. Off, a build without gamend drops tungstenite, rustls and the frame codec; Gamend's realtime socket runs on them. |
nothing |
webtransport |
on | on | 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, glow, kiss3d, wgpu, winit, cursor-icon, … (20 crates) |
What the web runtime 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.2 | none |
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 | bmp, exr, hdr, jpeg, png, tga, webp |
rodio |
0.22.2 | cpal, dither, experimental, flac, mp3, mp4, noise, playback, rand, rand_distr, symphonia, symphonia-aac, symphonia-flac, symphonia-isomp4, symphonia-mp3, symphonia-ogg, symphonia-pcm, symphonia-vorbis, symphonia-wav, vorbis, wasm-bindgen, wav |
rapier3d |
0.36.0 | alloc, debug-render, dim3, enhanced-determinism, f32, fem, serde-serialize, std |
parry3d |
0.31.1 | alloc, dim3, downcast-rs, ena, enhanced-determinism, f32, hashbrown, indexmap, required-features, rstar, serde, serde-serialize, serde_arrays, slab, smallvec, spade, std |
cosmic-text |
0.19.0 | monospace_fallback, shape-run-cache, std, swash, sys-locale |
wesl |
— | not in the runtime |
Dependency levers
What is still in a game template and is not a cargo feature. Sizes are given where measured.
- The WebGL2 fallback.
egui-wgpu's default features turn onwgpu/webgl: the GLES backend (glow) and naga beside WebGPU, for browsers without WebGPU. naga alone is 0.56 MB. - The tonemapping table. 0.9 MB raw, 0.2 MB brotli, for HDR tonemapping in kiss3d. An analytic curve costs no bytes.
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. -Zbuild-std with panic_immediate_abort
would cut more and needs nightly, which the pinned toolchain rules out.