Riadh Mnasri
← Back to blog
3 min read

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

AspectClient-side analysis (WASM)Server-side analysis
Infrastructure costZero, independent of user countGrows with concurrent calculations
Perceived latencyDepends on the user's deviceConstant, controlled by the server
Consistency across usersCan vary by hardware (depth, speed)Identical for everyone
Position privacyNever leaves the browserPasses through the server
Setup complexityCOOP/COEP headers, Web Worker managementA 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."