Most of the wasm material floating around is about the toolchain: install Emscripten, pass these flags, ship the .wasm. The decision that comes before it gets far less attention, and for this crowd it is the more interesting one, so here is the version I would have wanted to read first.
What A Module Actually Is
WebAssembly is a binary instruction format, not a language you write. You compile C, C++ or Rust into a .wasm file, the browser validates it and turns it into real machine code, and that compile step is where the speed comes from. There is no warm-up period while a JIT works out which functions are hot, so a wasm function runs at full speed from its first call.
What surprises people is how little a module can do on its own. It has no DOM, no canvas, no network, no audio. It gets a block of linear memory, a set of exported functions JavaScript can call, and a set of imported functions JavaScript hands to it. Everything it does to the outside world crosses that boundary. Sandbox with a bridge is the mental model, and nearly every wasm performance surprise traces back to it.
The Boundary Is The Whole Design
Calling into a wasm function is fast. It is not free. Cross the boundary a few thousand times a frame with tiny calls and you can spend the entire advantage you compiled for.
The pattern that works is a coarse boundary. Pass data in batches, do as much work as possible inside the module before returning, and share state through linear memory instead of copying values back and forth. Rather than asking the module for each entity's position one call at a time, let it write every position into a shared buffer and read the whole buffer once on the JavaScript side.
This is also why a wasm rewrite of ordinary game logic often benchmarks slower than the JavaScript it replaced. The logic got faster and the plumbing got more expensive, and the plumbing won.
For C and C++ it is Emscripten, which does much more than emit a binary. It ships a C library, maps OpenGL onto WebGL, simulates a filesystem, and generates the glue that loads the module and wires it to the page. Point it at an existing SDL or OpenGL game and with the right flags you get something playable without rewriting the source.
For Rust the support lives in the language ecosystem. wasm32-unknown-unknown is a first-class target, wasm-bindgen generates bindings with real types instead of raw numbers, and macroquad or Bevy sit on top depending on how ambitious the project is. Rust is what most people reach for when they are starting fresh rather than porting.
C#, Go and AssemblyScript all target wasm too, but for games those two paths have the deepest ecosystems and the most shipped titles behind them.
Your Engine Already Made This Choice
If you export from Unity or Godot, you are already shipping wasm. Both compile their C++ runtime to a module and drive a WebGL canvas from it, which explains the complaint everyone has: a trivial Unity web build is many megabytes because the engine runtime is the download, not your game. The load time you see is the browser compiling that binary, and streaming compilation is what hides most of it behind the download.
We put the longer version together on our own site, WebAssembly for web games, which goes through the toolchains, how each engine handles its web export, and the things wasm still cannot do without JavaScript holding its hand.
Reach for wasm when you have a hot loop running thousands of times per frame, a large existing native codebase, or a workload like physics or a software rasterizer where fixed types and explicit memory genuinely pay. Skip it when the honest answer is that your game logic runs fine in JavaScript. A build step you did not need is a real cost, and the speedup you imagined is often a rounding error.