JavaScript WebAssembly Table
| Piece | What it does | Field note |
|---|---|---|
WebAssembly.instantiateStreaming | Compile while downloading | fetch + compile in one pass - the fast path (needs application/wasm MIME) |
Linear memory | The wasm heap | An ArrayBuffer the module owns - growable, shared with JS by reference |
exports / imports | The boundary | Functions cross as callable stubs - numbers cheap, strings need marshaling |
No DOM in wasm | Compute only | No direct page access - wasm computes, JS renders (same split as workers) |
SharedArrayBuffer + threads | Real parallelism | Behind COOP/COEP headers - true threads for wasm modules |
Text format vs binary | wat is for humans | .wat readable, .wasm ships - always view disassembly in devtools |
Ported libraries | The killer app | ffmpeg, sqlite, Photoshop filters - decades of C/C++ now in the browser |
Not a JS replacement | A compute coprocessor | DOM glue, UI, and networking stay JS - wasm accelerates kernels |
WebAssembly is a portable binary instruction format the browser executes near-native: compile while downloading with instantiateStreaming, share data through linear memory (an ArrayBuffer the module owns), and cross into JavaScript through exports and imports.
Bottom line: wasm is a compute coprocessor, not a JavaScript replacement - the hot kernel (codecs, physics, image filters, sqlite) goes to wasm; DOM glue, UI and networking stay JavaScript. The boundary has a cost curve: numbers cross cheaply, strings and objects need marshaling, so architectures minimize crossings per frame.
The honest part: wasm's browser story is really the ported-library story - decades of C and C++ (ffmpeg, sqlite, Box2D, compression codecs) compiled to .wasm and suddenly running client-side. Writing raw wasm by hand is rare; integrating a compiled module is the everyday task.
How to use
- Stream the compile: WebAssembly.instantiateStreaming(fetch('app.wasm')) compiles while downloading - the fast path, needing the application/wasm MIME from your server.
- Move data through linear memory: write bytes into memory.buffer via a typed array, call the exported function, read results back - no string marshaling in hot loops.
- Reserve threads for real kernels: shared memory plus Workers behind COOP/COEP headers - real parallelism, but a deployment-level commitment, not a flag.
Frequently asked questions
What is linear memory and how does data cross the wasm boundary?
Memory is a growable ArrayBuffer the module owns; both sides see the same bytes. wasm code reads and writes it directly at machine speed; JavaScript accesses it through typed-array views (new Uint8Array(memory.buffer)). Crossing is therefore memory writes plus a function call - cheap for numbers and byte buffers, expensive for strings and objects, which have no native wasm representation and must be serialized into memory (length-prefixed UTF-8 is the convention). This cost curve is the architecture driver: passing a 4MB pixel buffer pointer is free; passing 10,000 strings per frame is the performance bug.
Why can't WebAssembly touch the DOM directly?
By design - the MVP keeps the surface minimal and portable. wasm has no knowledge of DOM, events or browser APIs; it computes and returns. Every page interaction goes through JavaScript glue: wasm returns a result, JS applies it to the DOM. This is less a limitation than a stability contract - the wasm instruction set changes glacially (backwards-compatible extensions), which is what lets a .wasm module from 2019 keep running, and it cleanly splits responsibilities: the compute kernel stays portable (the same module runs server-side or in workers), the platform-facing layer stays in the language that owns the platform.
What do COOP/COEP headers have to do with WebAssembly threads?
SharedArrayBuffer - the memory type real wasm threads require - was restricted after Spectre-class attacks: it is only exposed to cross-origin-Isolated documents, which means the COOP (Cross-Origin-Opener-Policy) and COEP (Cross-Origin-Embedder-Policy) headers must be set, and every embedded third-party resource must cooperate (CORP or CORS). The practical weight: enabling threads is a deployment-wide commitment that can break embedded ads, analytics and fonts that do not send cooperation headers. Teams either wrap the whole origin in isolation or load wasm threads in a dedicated isolated scope - measure whether your kernel is actually parallel before paying.
When does a workload actually justify WebAssembly?
When a hot, well-defined kernel dominates the profile AND a better-optimized implementation exists in C/C++/Rust. The winning cases are concrete: ffmpeg-quality video transcode in the browser, sqlite for local-first apps, physics engines, image/audio codecs, crypto primitives. The losing cases are common: general UI logic (JS is faster to ship and fast enough), small scripts where the 100-500KB runtime and compile cost dwarf the workload, and anything DOM-shaped. The honest gate is a profile: if the JS hot loop is already under a frame budget, wasm adds payload and boundary complexity for nothing - and if a native-quality library exists as wasm, integration beats reimplementing it in JS badly.