Versions and Feature Detection
WebAssembly grows by proposals. Each one goes through a public process in the WebAssembly Community Group, is implemented in engines, and once it is finished, is folded into the next version of the specification. This chapter lists what each version contains, explains how a module can depend on newer features safely, and shows how to detect them at runtime.
#The versions18.1
WebAssembly 1.0, the MVP, standardised in 2019 but shipping in browsers since 2017: the four number types, functions, structured control flow, one linear memory, one table of functions, globals, imports and exports. Everything in Parts I to IV that is not marked otherwise.
WebAssembly 2.0, finished in 2022, collected the proposals that had shipped since:
| Feature | What it adds | Chapter |
|---|---|---|
| sign extension | i32.extend8_s and friends |
4 |
| non-trapping float-to-int | the trunc_sat instructions |
4 |
| multiple values | multiple results, and blocks with parameters | 5, 6 |
| bulk memory | memory.copy, memory.fill, passive segments |
9 |
| reference types | externref, multiple tables, table.get and friends |
11 |
| mutable globals | importing and exporting mutable globals | 11 |
| fixed-width SIMD | the v128 type and its instructions |
19 |
WebAssembly 3.0, finished in 2025:
| Feature | What it adds | Chapter |
|---|---|---|
| garbage collection | struct and array types managed by the engine | 22 |
| typed function references | non-null references, call_ref |
22 |
| exception handling | tags, throw, try_table, exnref |
20 |
| tail calls | return_call and friends |
21 |
| memory64 | 64-bit addresses | 9 |
| multiple memories | more than one memory per module | 9 |
| extended constant expressions | arithmetic in initialisers | 11 |
| relaxed SIMD | faster SIMD with some non-determinism | 19 |
| JS string builtins | fast JavaScript strings, in the JS API | 16 |
Threads, despite shipping in every browser since around 2020, are still a separate proposal on the way to standardisation: the atomic instructions and shared memories of Chapter 23.
Proposals still in progress, as this book is written, include stack switching (for coroutines and effect handlers), JSPI (JavaScript promise integration, which lets synchronous WebAssembly code wait for a JavaScript promise), branch hinting, wide arithmetic (128-bit multiplication), custom page sizes, and memory control (unmapping and protecting pages).
#Support in browsers18.2
Every current version of Chrome, Edge, Firefox and Safari supports all of
WebAssembly 2.0, and threads when the page is cross-origin isolated
(Chapter 23). Support for the 3.0 features arrived at different times in
different browsers: garbage collection and tail calls have been available
everywhere since early 2025, while memory64 and some others reached Safari
later than the others. The WebAssembly project keeps a current table at
webassembly.org/features, and that, rather than any book, is the place
to check.
For a game shipping today, the practical baseline is WebAssembly 2.0 plus whichever 3.0 features your language needs and your audience's browsers support. Everything in Parts I to V of this book is in that baseline.
#Why feature detection matters18.3
A module that uses an instruction an engine does not know fails to
compile. There is no partial loading and no fallback: one unknown opcode
anywhere, even in a function that is never called, and the whole module is
rejected with a CompileError. So a program that wants to use a newer
feature where it is available, without breaking where it is not, must
ship two builds and choose between them before compiling either.
The way to choose is to ask the engine. Not by looking at the browser's version, which is unreliable, but by asking it to validate a tiny module that uses the feature. If it validates, the feature is there.
#Detecting features18.4
Here are four probes. Each is the smallest module that uses its feature:
(module
(func (result v128) (v128.const i32x4 0 0 0 0)))
(module
(func $f (return_call $f)))
(module
(type $t (struct (field i32)))
(func (result anyref) (struct.new $t (i32.const 0))))
(module
(func (result exnref) (ref.null exn)))
import fs from 'node:fs';
for (const feature of ['simd', 'tail', 'gc', 'exnref']) {
const bytes = fs.readFileSync(`${feature}.wasm`);
console.log(feature.padEnd(8), WebAssembly.validate(bytes) ? 'supported' : 'not supported');
}
simd supported tail supported gc supported exnref supported
Node 22 supports all four. In a page you would embed the probe modules as byte arrays in the JavaScript, since they are tiny:
node -e "console.log(JSON.stringify([...require('fs').readFileSync('simd.wasm')]))"
[0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,22,1,20,0,253,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11]
and choose a build:
const simd = new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,
10,22,1,20,0,253,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11]);
const url = WebAssembly.validate(simd) ? 'game.simd.wasm' : 'game.wasm';
const { instance } = await WebAssembly.instantiateStreaming(fetch(url), imports);
The wasm-feature-detect package on npm is a ready-made collection of
probes like these, one for every proposal.
Validation alone cannot detect everything. Threads need a shared memory,
which a browser allows only on a cross-origin isolated page, so the probe
for threads must check crossOriginIsolated too. JavaScript API features,
such as the JS string builtins, are detected by trying the API.
#Telling tools which features you use18.5
The tools in this book accept every standard feature by default. Older
tools, and some current ones, need to be told: wabt's tools take flags
such as --enable-tail-call or --enable-all; Binaryen's take
--enable-gc, --enable-simd or --all-features. When a tool rejects a
module that an engine accepts, an unenabled feature is the usual reason.
#What you have seen18.6
WebAssembly grows through proposals, gathered into versions: 2.0 added multiple values, bulk memory, reference types and SIMD; 3.0 added garbage collection, exceptions, tail calls, memory64 and more. A module using an unsupported feature fails to compile entirely, so programs that want newer features ship several builds and pick one by validating tiny probe modules at runtime.
The following chapters take the most important newer features one at a time, starting with SIMD.