Building a Game with Bevy for the Web

This page answers one task: you are building a game with Bevy, the Rust game engine, and want it playable in a browser — not just on desktop — with reasonable download size, correct rendering, working input and audio, and acceptable frame rates.

Prerequisites

  • [ ] A Bevy project that runs natively.
  • [ ] Rust with the wasm32-unknown-unknown target, plus wasm-bindgen-cli (matching your lockfile) or trunk.
  • [ ] A static server for testing, and target browsers for WebGL2 and WebGPU.

How Bevy runs in the browser

Bevy compiles to wasm32-unknown-unknown like any Rust crate. Rendering goes through wgpu, which on the web targets either WebGL2 (widely supported) or WebGPU (faster and closer to native, supported in current Chromium-based browsers and increasingly elsewhere). Windowing becomes a <canvas> element; input comes from browser events; audio uses Web Audio; assets load over HTTP instead of from disk. The ECS, game logic and most plugins work unchanged.

The main differences from native are threads and size. Bevy’s multi-threaded executor uses a single thread on wasm32-unknown-unknown by default, so systems that run in parallel natively run sequentially in the browser. And a Bevy game is a large Wasm binary — tens of megabytes unoptimised — so size reduction is part of shipping it.

A Bevy game in the browser Game systems and the ECS run in Wasm on a single thread. Rendering goes through wgpu to WebGPU or WebGL2 on a canvas. Input arrives from browser events, audio plays through Web Audio, and assets are fetched over HTTP from the server. game systems + ECS single-threaded in Wasm wgpu renderer WebGPU or WebGL2 canvas + browser events window, input Web Audio unlocked by user gesture HTTP asset loading assets/ from the server

Step 1 — build and run for the web

The quickest loop uses wasm-server-runner, which runs cargo run --target wasm32-unknown-unknown as a local web server:

# .cargo/config.toml
[target.wasm32-unknown-unknown]
runner = "wasm-server-runner"
cargo install wasm-server-runner
cargo run --target wasm32-unknown-unknown

For release builds and deployment, trunk (with an index.html that references the crate) or a manual wasm-bindgen --target web step produces static files: an HTML page, the .wasm, the JavaScript glue, and your assets/ directory.

Step 2 — choose WebGL2 or WebGPU

Bevy’s feature flags select the web backend. WebGL2 runs nearly everywhere but limits some rendering features (compute shaders, some texture formats, storage buffers); WebGPU supports Bevy’s full renderer but requires a browser with WebGPU. Many games ship WebGL2 for reach, or build both and choose at startup by checking navigator.gpu. Test visually on both: lighting, shadows and post-processing can differ, and some features silently fall back.

[dependencies]
bevy = { version = "0.x", default-features = false, features = ["bevy_render", "bevy_core_pipeline", "bevy_sprite", "bevy_winit", "webgl2"] }

Disabling default features and enabling only what the game uses also cuts binary size substantially.

Step 3 — fit the canvas and handle input

Configure the primary window to attach to a specific canvas and follow its size:

App::new().add_plugins(DefaultPlugins.set(WindowPlugin {
    primary_window: Some(Window {
        canvas: Some("#game".into()),
        fit_canvas_to_parent: true,
        prevent_default_event_handling: true,   // stop the page scrolling on arrow keys / space
        ..default()
    }),
    ..default()
}));

Keyboard, mouse, touch and gamepad input map to Bevy’s input resources. Pointer lock and fullscreen require a user gesture — request them from a click handler. Browsers deliver some keys to the page or browser chrome (Tab, F-keys, shortcuts); avoid binding gameplay to them.

WebGL2 versus WebGPU backends for a Bevy web build The WebGL2 backend runs in nearly all browsers but lacks compute shaders and some formats, so some rendering features are unavailable. The WebGPU backend supports Bevy's full renderer with better performance but needs a WebGPU-capable browser, so many games detect support and choose at startup. WebGL2 nearly universal support no compute shaders some features fall back maximum reach WebGPU full Bevy renderer better GPU performance newer browsers only best visuals

Step 4 — load assets over HTTP

AssetServer::load("sprites/player.png") fetches assets/sprites/player.png relative to the page. Assets load asynchronously, so show a loading state and wait for them before starting gameplay (Bevy’s asset states or a loading plugin). Large assets dominate download time: compress textures (KTX2 with Basis Universal is supported by Bevy and transcodes on the GPU), use compressed audio formats, and serve everything with long-lived cache headers and Brotli or gzip.

Step 5 — shrink the binary

A release Bevy build can exceed 30 MB before optimisation. Apply size settings and measure:

[profile.wasm-release]
inherits = "release"
opt-level = "z"        # or "s" — measure frame rate too
lto = "fat"
codegen-units = 1
strip = "debuginfo"

Then run wasm-opt -Oz (or -O3 if speed suffers) on the output and serve it compressed. Disabling unused Bevy features usually saves more than any flag. Typical small games end up in the 5–15 MB range uncompressed and a fraction of that with Brotli.

Audio unlock

Browsers start audio contexts suspended until the user interacts with the page. Bevy’s audio resumes after a user gesture, but sounds triggered before then are lost or delayed. Start the game behind a “Click to play” screen, which both unlocks audio and gives assets time to load.

Performance compared with native

Expect lower frame rates than native, mainly from single-threaded system execution, the cost of WebGL2 on some GPUs, and the overhead of the browser’s GPU process. Profile with the browser’s Performance panel, which shows Wasm frames; Bevy’s diagnostics plugins work in the browser too. Reducing per-frame allocations, entity counts and draw calls helps more on the web than natively. Threads for Bevy on the web require shared memory and atomics, plus cross-origin isolation; this setup is evolving, so check current Bevy documentation before relying on it.

Platform-specific code in a Bevy game

Most game code is portable, but a few systems differ on the web: saving games (files natively, browser storage on the web), opening URLs, quitting (there is no “exit” in a browser tab), window management, and anything touching the file system or native libraries. Gate these with #[cfg(target_arch = "wasm32")] and keep them in a small platform module, so gameplay systems never contain cfg blocks. For saves, a resource that abstracts “load slot / save slot” with one implementation per platform keeps game code clean. Remove or replace the “Quit” menu item in web builds, and handle the page being hidden (visibilitychange) by pausing the game, since browsers throttle timers and animation frames in background tabs and a game that keeps simulating on a stalled clock behaves strangely when the player returns.

Mobile browsers

Bevy web builds run on phones, but mobile adds constraints: touch input instead of keyboard and mouse, smaller memory budgets (large binaries and texture sets can push the tab towards being killed), GPU differences that make WebGL2 the safer choice, and thermal throttling during long sessions. Design touch controls explicitly — Bevy exposes touch events — and test on real devices early. Lower texture resolutions and simpler post-processing for mobile can be chosen at startup based on screen size or a quick benchmark, which keeps the desktop experience intact while making the game playable on phones.

Deployment

The output is static files, so any static host works: itch.io for game distribution, GitHub Pages, or a CDN. Make sure the host serves .wasm with application/wasm and compresses it, and use hashed filenames so updates are picked up while unchanged assets stay cached.

Expected output

The game runs at 60 fps in Chrome with WebGPU and in Firefox and Safari with WebGL2; the page shows a click-to-play screen that unlocks audio; assets load with a progress indicator; the canvas resizes with the window; arrow keys do not scroll the page; and the Wasm binary is 9 MB uncompressed, 2.6 MB with Brotli.

Gotchas

  • Default features on. Huge binaries. Enable only what the game uses.
  • WebGPU-only rendering. Many browsers lack it. Ship WebGL2 or detect and choose.
  • Gameplay starting before assets load. Missing textures. Wait for asset states.
  • Audio before user interaction. Silent. Use a click-to-play screen.
  • Mismatched wasm-bindgen versions. Builds fail. Pin the CLI to the lockfile version.
  • No pause on hidden tabs. Throttled timers make the simulation jump. Pause on visibilitychange.

Performance note

For a small 2D game, disabling unused default features cut the release Wasm from 31 MB to 14 MB; size settings plus wasm-opt -Oz brought it to 9 MB, and Brotli to 2.6 MB on the wire.

Bevy web build size after each step Megabytes of the Wasm binary for a small 2D Bevy game with default features, with unused features disabled, with size profile settings and wasm-opt, and the Brotli-compressed transfer size. MB default features 31 MB unused features off 14 MB opt-level z + LTO + wasm-opt 9 MB Brotli on the wire 2.6 MB

Frequently Asked Questions

Does Bevy’s ECS run multi-threaded in the browser? Not by default; systems run on one thread on wasm32-unknown-unknown.

Can I use Bevy’s UI and text? Yes — they render through the same pipeline.

How do I save game progress? Use localStorage or IndexedDB through web-sys, or a Bevy plugin that wraps them.

Should I use trunk or wasm-bindgen directly? Trunk is convenient for builds and dev servers; scripts with wasm-bindgen give finer control.

What happens when the player switches tabs? Browsers throttle background tabs; listen for visibilitychange and pause the game so the simulation does not jump when they return.

Does a Bevy web build work on phones? Yes, with touch controls, smaller textures and WebGL2; test on real devices early because memory and GPUs differ.

Where can I host a Bevy web game? Any static host — itch.io, GitHub Pages or a CDN — as long as .wasm is served with the right MIME type and compression.

How do I show loading progress? Track pending asset handles in a loading state and draw a progress bar until all required assets report loaded.

← Back to Graphics, Games & Simulation