Skip to content

Search

Documentation

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 the content-encoding header), 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-sources runs only in a build with compile.
  • fallback-fonts: egui's Hack, Ubuntu Light and two emoji faces. 1.4 MB of data. A game draws with the faces under its own fonts/ 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.

Where a pack's bytes are, and what lossless re-encoding takes off

[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 quantized and vorbis is 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-run prints 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
  • physics2d and physics3d each register one world. Without physics3d a template is 1.8 MB smaller raw and 0.4 MB brotli; without physics2d, 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, vorbis and wav each 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 a vorbis build.
  • The script needs the wasm32-unknown-unknown target, the wasm-bindgen CLI at the version in Cargo.lock (checked), wasm-opt from binaryen, and brotli for the size line it prints.
  • It refuses a module under 5 MB: a build whose entry point was optimised away.
  • http, websocket, webtransport, gamend and multiplayer gate 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 on wgpu/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.