Running Stockfish in the browser: what WebAssembly actually changes

On OpeningBook, every position gets analyzed by Stockfish directly in the browser, with no server call. WebAssembly is what makes that possible, but the buzzword hides concrete constraints that change how you design the application around it.
What WASM actually brings
Stockfish is written in C++, optimized for raw performance over years. Compiling that code to WebAssembly lets it run in the browser at near-native speed, without rewriting the engine in another language. That's the difference between porting twenty years of chess engine optimizations, versus reimplementing a position evaluator from scratch in JavaScript, with all the performance regressions that would imply.
The constraint you discover quickly: threading
The real trap isn't the compilation itself, it's multi-threaded execution.
Stockfish uses multiple cores to parallelize its search, but multi-threaded
WebAssembly in a browser requires SharedArrayBuffer, which in turn
requires precise HTTP security headers (Cross-Origin-Opener-Policy,
Cross-Origin-Embedder-Policy). Without those headers correctly configured
server-side (or on Vercel, in my case), the engine silently falls back to
single-threaded mode, with a performance loss that isn't obvious to diagnose
if you don't know where to look.
Browser
┌────────────────────────────────────────────────────────┐
│ OpeningBook page │
│ │ │
│ ▼ │
│ Web Worker ──▶ Stockfish.wasm ──▶ analysis result │
│ ▲ │
│ │ requires SharedArrayBuffer for multi-threading │
│ │ │
│ Required HTTP headers on the server response: │
│ Cross-Origin-Opener-Policy: same-origin │
│ Cross-Origin-Embedder-Policy: require-corp │
└────────────────────────────────────────────────────────┘
Without those two headers, the browser silently refuses to allocate a
SharedArrayBuffer shared across threads, and Stockfish keeps working, but
in single-threaded mode, with no visible console error pointing to the
actual cause.
The resulting architecture trade-off
Running analysis client-side rather than server-side changes the distribution of responsibilities: no analysis API to maintain, no server cost that grows with the number of concurrent users, but in exchange, every device needs to be capable of running a properly optimized chess engine. On a recent computer, the difference is invisible. On an older device, the experience degrades, and you need a fallback (reduced analysis depth, or a UI that clearly signals the calculation is taking time).
Client vs server, the trade-off summarized
| Aspect | Client-side analysis (WASM) | Server-side analysis |
|---|---|---|
| Infrastructure cost | Zero, independent of user count | Grows with concurrent calculations |
| Perceived latency | Depends on the user's device | Constant, controlled by the server |
| Consistency across users | Can vary by hardware (depth, speed) | Identical for everyone |
| Position privacy | Never leaves the browser | Passes through the server |
| Setup complexity | COOP/COEP headers, Web Worker management | A classic HTTP API |
The privacy row isn't a minor detail for a family tool: a chess position analyzed by a child has no reason to pass through a server, and the WASM choice keeps it entirely local by construction, not by additional configuration.
Why this choice, despite the complexity
For a family tool like OpeningBook, where each position analysis is consulted occasionally rather than continuously, the absence of a recurring server cost heavily outweighs the initial setup complexity. That trade-off doesn't generalize: on a product with thousands of concurrent users and analysis consistency requirements, a centralized server-side engine would probably still be the better choice. The right architecture choice always depends on the actual usage profile, not an a priori preference for "everything client-side."