Web builds
A bevy-react app also runs in the browser. The Bevy app compiles to
wasm32-unknown-unknown, and the same vendor.js and app.js bundles run in
the browser's own JS engine instead of the embedded V8. The UI is still
bevy_ui drawn into Bevy's canvas, not DOM. No code changes are needed: the
bundles detect the browser host at startup. The
live demo is the demos app built
this way.
Usage
One-time setup:
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cliInstall the wasm-bindgen CLI at the version of the wasm-bindgen crate in
your Cargo.lock (cargo install wasm-bindgen-cli --version <x.y.z>). A CLI
a patch or two newer usually works; a larger gap makes wasm-bindgen fail.
Build the three parts into one folder, here ui/dist/ for a package whose
binary is my_game:
(cd ui && npx bevy-react build) # dist/vendor.js + dist/app.js
cargo build --target wasm32-unknown-unknown
wasm-bindgen --target web --out-dir ui/dist --out-name game \
target/wasm32-unknown-unknown/debug/my_game.wasm
cp -r assets ui/dist/assetswasm-bindgen writes game.js (the loader) and game_bg.wasm. Pick an
--out-name other than app or vendor, so the loader doesn't overwrite a
bundle.
Add an index.html to ui/dist/ with a <script type="module"> that boots
the parts in this order:
import init from "./game.js";
await init(); // runs your Rust `main`: builds the App, starts Bevy
await loadScript("./vendor.js");
await loadScript("./app.js");
function loadScript(src) {
return new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = src;
s.onload = resolve;
s.onerror = () => reject(new Error(`failed to load ${src}`));
document.body.appendChild(s);
});
}Then serve the folder over HTTP, for example with npx serve ui/dist.
Browsers don't load wasm from file:// URLs. All paths are relative, so the
site also works from a subdirectory.
For a release build, use npx bevy-react build --prod and
cargo build --release, and point wasm-bindgen at
target/wasm32-unknown-unknown/release/.
Load order
The order in the page script is required:
init()first. It runs yourmain. While theAppbuilds,ReactUiPlugininstalls the browser host onglobalThis.__bevyHost, the object the React runtime sends its ops to.App::runthen schedules Bevy's frame loop and returns, soinit()resolves.vendor.jsnext. It reads__bevyHostwhen it is evaluated. Loaded beforeinit()resolves, it falls back to the native host and throws (Deno is not defined).app.jslast. It takes React and the runtime fromvendor.js.
Both bundles are classic scripts, not ES modules: load them with plain
<script> elements as above. React can render as soon as app.js runs. The
first commits are held until the Bevy app has finished its asynchronous
renderer setup, then applied in order, so there's nothing to wait for.
The Rust side
Your main is the wasm entry point unchanged. A few things differ:
ReactUiPlugin::new(path)andhot_reload(..)are ignored: the page loads the bundles, and nothing watches them.- Gate native-only code with
#[cfg(not(target_arch = "wasm32"))]. This includes the--export-bindingsexporter from TypeScript codegen: generatebevy.tswith a native run. - Bevy's default features render through WebGL2 (the
webgl2feature).
To make the canvas fill the page and follow the browser window, let it track its parent element:
let window = Window {
fit_canvas_to_parent: true,
..default()
};
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(window),
..default()
}));Give html and body a full height with no margin and overflow: hidden.
On phones, also set touch-action: none on the canvas: without it the browser
turns touch drags into page scrolling and pinch-zoom instead of passing them
to the app.
bevy-react enables the browser backend of getrandom for you. If the wasm
build still fails inside getrandom, copy the wasm32-unknown-unknown
section of the repository's .cargo/config.toml
into your project.
Assets
Bevy's default AssetPlugin fetches assets over HTTP from assets/ next to
the page. Copy your asset folder to dist/assets/: textures, fonts given to
default_font(..) and font(..), cursor images and custom filter shaders all
load from there. If the native build points AssetPlugin somewhere else (the
demos use file_path: "../assets"), keep the default on the web.
On GitHub Pages, add an empty .nojekyll file to the site so every file is
served as is.
Differences from native
- No hot reload. Rebuild and reload the page:
npx bevy-react build --watchstill rebuildsapp.json change, but the page doesn't pick it up, and a reload resets all React state. See Hot reload for the native workflow. - One thread. React runs on the page's main thread, between Bevy frames, where on native it has a thread of its own. A slow render delays the next frame. Events from Bevy are delivered at the end of each frame, and the ops React commits in response apply on the next one.
- Logs go to the browser console. That covers
console.*from React, Rust panics and Bevy's log output. - Devtools run in debug builds with a few gaps: the panel's layout isn't saved between sessions, the Console tab stays empty (use the browser's console), and some timing columns in the Bridge tab are not measured. See Devtools.
Reference setup
The demos app builds for the web with one script,
examples/demos/ui/build-web.mjs:
it builds both bundles with buildVendor and buildApp from
bevy-react/build-lib, compiles the Bevy app to wasm, runs wasm-bindgen,
copies index.html and the assets
into dist/, and serves it. Its main and window setup are in
examples/demos/main.rs. Run it from the
repository root:
npm run build:web -w demos # debug build, then serve
npm run build:web:prod -w demos -- --build-only # release build, no serverLimits
- wasm builds need a lot of disk space: keep tens of GB free.
cargo clean --target wasm32-unknown-unknownreclaims it. - Integers above 2^53 in event or response payloads can't be sent to React on
the web (they are dropped with a console error); on native they arrive as a
BigInt.
bevy-react