SharedArrayBuffer and Atomics: True Shared Memory in the Browser
JavaScript is single-threaded. Web Workers provide parallelism, but the default communication mechanism — postMessage — copies data. Sending a 50MB buffer between a main thread and a worker means 50MB is duplicated in memory before the worker receives it. With Transferable objects, you can avoid the copy, but the sender loses ownership.
SharedArrayBuffer solves this by providing a single region of memory that multiple threads can read and write simultaneously without copying or transferring.
What is SharedArrayBuffer?
A SharedArrayBuffer is a fixed-size binary buffer — like ArrayBuffer — but instead of being owned by one context, it can be shared across multiple Workers and the main thread. All contexts operate on the same physical memory.
// Main thread
const sab = new SharedArrayBuffer(1024) // 1KB shared memory
const view = new Int32Array(sab)
view[0] = 42
// Send to worker — not copied, not transferred
worker.postMessage({ buffer: sab })
// Worker
self.onmessage = (e) => {
const view = new Int32Array(e.data.buffer)
console.log(view[0]) // 42 — reading the SAME memory
view[0] = 99 // main thread sees this change immediately
}
Both the main thread and the worker are literally reading and writing the same bytes.
The Need for Atomics
Shared mutable memory introduces data races. Consider two workers incrementing a counter:
// Not safe — both workers read 0, both write 1, result is 1 not 2
view[0] = view[0] + 1
Reading, incrementing, and writing are three separate operations. If both workers execute the read before either executes the write, the increment is lost.
Atomics is a namespace object providing operations that are guaranteed to be atomic — they cannot be interrupted mid-operation by another thread:
// Safe — atomic increment
Atomics.add(view, 0, 1) // read-modify-write as single uninterruptible operation
The Atomics API covers:
Atomics.load(view, index)— atomic readAtomics.store(view, index, value)— atomic writeAtomics.add,Atomics.sub,Atomics.and,Atomics.or,Atomics.xorAtomics.compareExchange(view, index, expectedValue, replacementValue)— CAS (compare-and-swap)Atomics.wait(view, index, expectedValue)— sleep until value changes (workers only)Atomics.notify(view, index, count)— wake sleeping workers
Cross-Origin Isolation Requirement
After the Spectre vulnerability disclosure in 2018, browsers disabled SharedArrayBuffer entirely. High-resolution timers (including those possible to construct with shared memory) could be used to exfiltrate cross-origin data through side channels.
It was re-enabled in 2020, but only for pages that opt into cross-origin isolation by serving two HTTP headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
These headers tell the browser the page agrees to strict isolation: no cross-origin iframes or resources unless they explicitly opt in with Cross-Origin-Resource-Policy. Without these headers, SharedArrayBuffer is undefined regardless of browser version.
Practical Use Cases
SharedArrayBuffer is a specialized tool. It is not a replacement for postMessage in most applications. It shines in:
Game engines and simulation: Multiple workers computing physics, AI, and rendering can share a large game state buffer without copying per frame.
Audio processing: AudioWorklet can read shared buffers directly with minimal latency, avoiding the overhead of postMessage for each audio frame.
WebAssembly threads: WASM's thread proposal uses SharedArrayBuffer as its memory backing, enabling compiled C/C++/Rust code with true threading.
Video codec implementations: Frame buffers shared between decode workers and the rendering pipeline.
A Simple Worker Pool Example
// main.js
const sab = new SharedArrayBuffer(4 * 4) // 4 Int32 slots
const counter = new Int32Array(sab)
const workers = Array.from({ length: 4 }, () => new Worker('/worker.js'))
workers.forEach(w => w.postMessage({ buffer: sab }))
// worker.js
self.onmessage = ({ data }) => {
const counter = new Int32Array(data.buffer)
// Each worker increments the shared counter 1000 times safely
for (let i = 0; i < 1000; i++) {
Atomics.add(counter, 0, 1)
}
self.postMessage('done')
}
After all four workers finish, counter[0] will be exactly 4000 — no lost increments.
Limitations
Atomics.wait()blocks the thread and is forbidden on the main thread (it would freeze the UI)- Only typed array views work with Atomics (
Int32Array,BigInt64Array, etc.) SharedArrayBufferonly works in contexts that are cross-origin isolated- Debugging shared memory race conditions requires tools like thread sanitizers — browser devtools have limited support
Conclusion
SharedArrayBuffer with Atomics brings true shared memory parallelism to JavaScript, enabling performance that was previously only possible in native code. It is not a general-purpose tool — most web applications have no need for it. But for high-performance computation, game engines, or any workload where the cost of copying data between threads is significant, shared memory is the only solution that avoids that cost entirely.
