WebAssembly.Memory: Share Data Between Web Workers
This article explains how to use the WebAssembly.Memory
API to share data efficiently between Web Workers. You will learn how to
create a shared WebAssembly memory buffer, pass it to multiple workers,
and read or write data concurrently without the overhead of structured
cloning.
Understanding Shared WebAssembly Memory
By default, Web Workers communicate by passing messages via the
postMessage API, which copies data using the structured
clone algorithm. For large datasets, this copying mechanism introduces
significant performance overhead.
The WebAssembly.Memory API offers a high-performance
alternative. By configuring WebAssembly memory to be shared, multiple
execution threads (the main thread and Web Workers) can read and write
to the same underlying raw binary buffer
(SharedArrayBuffer) simultaneously.
Step 1: Security Requirements
Because shared memory relies on SharedArrayBuffer under
the hood, modern browsers require strict security headers to mitigate
side-channel attacks like Spectre. Your server must serve the HTML
document with the following HTTP headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Without these headers, creating a shared
WebAssembly.Memory instance will fail.
Step 2: Creating Shared WebAssembly Memory
To create a shared memory space, instantiate
WebAssembly.Memory with the shared: true
property. When shared is set to true, you must
also specify a maximum page size (each WebAssembly page is
64 KB).
// Create a shared memory object
// initial: 10 pages (640 KB), maximum: 100 pages (6.4 MB)
const sharedMemory = new WebAssembly.Memory({
initial: 10,
maximum: 100,
shared: true
});Step 3: Passing the Memory to Web Workers
Once the memory object is created, you can send it to your Web
Workers using postMessage. Unlike standard objects, the
memory reference is passed directly, allowing workers to access the
exact same buffer.
// Initialize Web Workers
const worker1 = new Worker('worker.js');
const worker2 = new Worker('worker.js');
// Send the shared memory instance to both workers
worker1.postMessage({ memory: sharedMemory });
worker2.postMessage({ memory: sharedMemory });Step 4: Accessing and Modifying Memory in the Worker
Inside the worker file (worker.js), listen for the
message containing the memory object. You can access the raw buffer
using memory.buffer and wrap it in a JavaScript TypedArray
(such as Uint8Array or Int32Array) to read and
write data.
// worker.js
self.onmessage = function (event) {
const { memory } = event.data;
// Create a view to read/write 8-bit unsigned integers
const view = new Uint8Array(memory.buffer);
// Write data to index 0
view[0] = 42;
console.log(`Worker read value: ${view[0]}`);
};Because both workers and the main thread point to the same
memory.buffer, any modification made by one thread is
instantly visible to all other threads.
Managing Race Conditions with Atomics
When multiple threads access the same memory location simultaneously,
race conditions can occur. To prevent data corruption, use the
JavaScript Atomics API when reading or writing to the
shared buffer.
// worker.js
self.onmessage = function (event) {
const { memory } = event.data;
const view = new Int32Array(memory.buffer);
// Safely add 5 to index 0 using Atomics
Atomics.add(view, 0, 5);
// Safely load the value
const value = Atomics.load(view, 0);
console.log(`Safe value: ${value}`);
};