WoWOnchain
Docs

The browser client

How a 2006 game client ended up in a browser tab: Rust, WebAssembly and WebGPU, the game's files streamed instead of installed, a relay for the network, and the measured work that took a cold start from 25 seconds to 7.

8 min readUpdated Oct 14, 2026

Where it started

The client is benilla, an open-source reimplementation of the World of Warcraft 1.12.1 client written in Rust on the Bevy engine (by Sam, MIT or Apache-2.0). Upstream built a desktop client over 569 commits. It reads the original game's data formats and runs the original interface code, so it looks and behaves like the client people played in 2006.

We forked it and took it into the browser. The port so far:

Commits on top of upstream209, between 7 and 12 October 2026
Linesabout 104,000 added: 34,000 in our crates, 64,000 in patched third-party code
Crates25 (upstream has 19): the client plus a host layer, a browser storage layer, the relay, the web server, the wallet sign-in service and a test probe
First play in a browser7 October 2026: sign-in, realm list, character creation, Northshire, chat, logout, reload, at 90 fps
On its own serversince 14 October 2026

The stack

LayerWhat we use
LanguageRust, stable toolchain (no nightly features)
EngineBevy 0.18
Graphicswgpu 27 on WebGPU. No WebGL fallback: WebGPU is what makes it run at full speed
Targetwasm32-unknown-unknown through wasm-bindgen
InterfaceLua 5.1.5 compiled to WebAssembly, running the stock 1.12 interface code (FrameXML and GlueXML) and addons
Soundthe kira mixer on its own AudioWorklet thread
Storagethe browser's private file system (OPFS) for settings and the game-data cache
NetworkWebSocket to our relay, which speaks the realm's own protocol

You need Chrome or Edge 137+, or Firefox 141+. Besides WebGPU, the interface depends on WebAssembly exception handling: that's how Lua's error recovery works in a page. The Play page tests both in your browser.

What a browser doesn't have

A game client assumes things a web page can't do. Each one needed its own answer.

A scripting language written in C

The whole 1.12 interface is Lua. We compile Lua 5.1.5 itself to WebAssembly with clang, and map its setjmp/longjmp error handling onto WebAssembly exceptions. The stock interface code runs unchanged; a 30-check smoke test passes natively, in Node and in the browser.

Files on a disk

The 1.12.1 client reads 12 archives, about 3.6 GiB and 79,733 files. Nobody wants to download that before playing, so nothing is installed:

  • The archives sit on a data host in 8 MiB pieces that never change. The client reads them with HTTP range requests, 16 KiB at a time, only the parts it needs.
  • A memory cache and a prefetch queue with priorities fetch what you'll need next (the zone ahead, the city you're flying to) without ever making a frame wait for the network.
  • A disk cache in the browser keeps what you've already loaded, so a second visit doesn't download it again.

A first visit to Northshire fetches about 125 to 130 MB. A return visit makes 3 to 10 requests and downloads almost nothing.

Raw network connections

Pages can't open TCP sockets, and the realm speaks TCP. The relay turns the page's WebSocket into the realm's protocol and back. It pumps each direction separately, turns Nagle off, pings every 30 seconds so proxies don't cut idle players, checks that connections come from our site, caps connections per network, and only ever connects to the realm. It adds 0.3 to 0.5 ms over a direct connection.

Threads

A page's code runs on one main thread. The game runs there: about a thousand engine systems a frame. Sound runs on an AudioWorklet (a second copy of the same WebAssembly module, mixing at 48 kHz on its own thread), and the disk cache on a worker. We also built a fully multithreaded build and an off-thread file decoder; both worked, and both are on the "tried and dropped" list below, with the numbers.

Graphics that don't freeze the page

Compiling GPU pipelines the normal way blocks the browser's GPU queue: 400 of them held it for 4.6 seconds in a bare test. We patched the graphics stack to compile them asynchronously, which freed the queue after 0.09 seconds.

Accounts and passwords

There are none to type. Phantom signs a sign-in message, the sign-in service checks it, maps the wallet to one realm account for good, and hands the client a fresh random password that works once. The client logs in with it on its own. See How it's built.

Tabs, settings, and the back button

  • Your key bindings, interface layout and addon settings live in the browser and are saved every minute and when you leave the page.
  • One tab per browser profile runs the game, so two tabs can't overwrite each other's settings.
  • A tab you can't see gets no frames, so the game pauses there. The realm's movement checks were tuned so a paused tab isn't mistaken for a cheat.

Measured, before and after

Every change below was measured on both sides of the change. These are the ones that mattered.

Getting into the game

ChangeBeforeAfter
The realm moved from a home PC to its own servercold page to login screen 25.5 s7.3 s
Brotli instead of gzip for the client23.1 MB download12.9 MB (1.2 s to download, 2.9 s to start)
Memory reserved once per load instead of growing step by stepcold Stormwind entry 12.5 s8.1 s
City building files fetched in one batchcold Stormwind entry 23.0 s14.3 s
Both loading fixes, Northshireloading screen 25 to 50 s9.7 s
Disk cache in the browserreturn visit 1,900 requests, 130 MB3 to 10 requests, about 0 MB
Asynchronous GPU pipelinespage blocked 851 ms per world entry86 ms
Game data from a data host instead of the home PCpage start to in world 27.0 s22.3 s

On the live server, Enter World takes 9.1 seconds the first time in Northshire and 5.3 to 6.2 seconds after that.

Frames

ChangeBeforeAfter
No tracing bookkeeping in the page (it leaked 24 MiB a minute)frame work p50 10.9 ms9.8 ms, leak gone
Ground decals clipped to what's visiblewalking in Northshire, slow-CPU test, p50 88 ms62 ms
Unused shadow maps droppedGPU memory 359 MiB317 MiB
Idle 3D portraits stop re-binding every frame63 GPU bindings a frame0

The GPU isn't what limits the frame rate: a frame costs 0.2 to 0.4 ms of GPU time at 1080p against 11 to 17 ms of CPU.

Network and sound

ChangeBeforeAfter
Relay pumps each direction separatelyyour moves while the server sends a burst: p50 741 ms0.27 ms
Moves and casts written before renderingkey press to packet p50 18.1 ms12.6 to 14.9 ms
Other players drawn from when they movedvisible hitches on steady movers 21087
The relay sees players' real addresses behind the tunnel16 of 40 test players admitted40 of 40
Sound on its own AudioWorklet threadinterface sound latency 192 ms, 5 dropouts entering the world51 ms, none
Sound effects warmed up before you need them9 to 16% of first plays silent in a fresh browser0 in 3 of 4 runs

The realm

ChangeBeforeAfter
World updates every 50 ms instead of 100chat p95 102 ms, others' moves p95 202 ms54 ms, 153 ms, same CPU
More database connections and worker threadsstall after a restart's login rush 9.3 s1.2 s
The Mithril bridge reads accounts in batches250 players: a coin settled in p50 18.2 s and falling behind1.9 s, keeping up

Tried and dropped

Measured, and not worth shipping yet:

IdeaWhat we measured
WebAssembly threadsWorks, but three workers on animation saved 0.2 to 0.7 ms of a 10.5 ms frame. About three weeks of work for 2 to 6%.
Decoding city files on a workerThe loading screen took 7.2 s either way: decoding was already under 0.6 s.
SIMD instructionsNo gain (11.4 vs 12.5 ms in Northshire, 10.9 vs 10.8 ms in Stormwind).
A more aggressive optimizer passNot faster, and 3.4 MB bigger.
A different memory allocatorSlower loading.
Render bundles for static scenerySame GPU time, no CPU saved.
An IndexedDB cacheCold entry 80 to 94 s with it, 47 to 50 s without. Replaced by the OPFS cache.

Where it runs

Server4 vCPU, 16 GB, New Jersey: the realm, its database, the Mithril bridge, the web services and this website
ExposureNo public ports except SSH. Every service listens on the machine itself; players arrive through a Cloudflare tunnel
Addressplay.wowonchain.com
Game dataa separate host in Amsterdam; every archive checked equal to its pieces
Backupsa nightly database dump, kept 14 days, plus the provider's server backups
The move14 October 2026, 10 minutes start to finish: every character, account and ledger row came across, and game and chain still agreed to the copper

On the server, the world updates every 50 ms (p50 50, p99 51), and the whole stack uses under 2 GB of its 16 with a player online.

What's next for the client

  • A smaller disk cache. A capped cache that keeps only the blocks you actually use (5 GiB on disk down to 34 MiB in our test) is built and ships next.
  • The movement clock. The browser's movement clock drifts slightly against the server's, so a few clock-based anticheat checks log for review instead of kicking until the client's fix lands. The checks that catch speed hacks, teleports, flying and wall climbing kick today.
  • More browsers. Chrome, Edge and Firefox today; Safari as its WebGPU support matures.
  • Game data closer to you. One data host today; more regions as the playerbase grows.
  • More cores. The game runs on one CPU thread, which a modern desktop handles easily; WebAssembly threads are proven to work and wait until they're worth it.
  • Room to grow. The Mithril bridge settles for about 230 to 250 players playing at once with one batch in flight; parallel lanes take it further when the realm needs it.

How each part was tested: Tested.

Spotted something wrong or out of date? The docs follow the server, so tell us in game or on our socials and it gets fixed.