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}`);
};