Articles

JavaScript Atomics and SharedArrayBuffer Explained

SharedArrayBuffer lets JS threads share raw memory; Atomics provides race-free operations on it. How true multi-threaded data sharing works in JS.

Takina Takina · · 3 min read
A code editor showing JavaScript on a dark background

SharedArrayBuffer is a JavaScript object representing a fixed-length block of raw memory that multiple threads — a main thread and one or more web workers — can read and write concurrently. Atomics is the companion API that provides race-free operations on that shared memory: reads, writes, and read-modify-write operations that are guaranteed not to be torn or interleaved by another thread acting on the same bytes at the same time.

Together, they’re the closest JavaScript gets to genuine shared-memory multithreading, as opposed to the message-passing concurrency (postMessage) that workers normally use.

Why regular ArrayBuffer isn’t enough

A standard ArrayBuffer is transferred, not shared, when you send it to a worker via postMessage with a transfer list — ownership moves to the receiving context, and the sender’s reference becomes unusable. That model is safe by construction: there’s never a moment where two threads hold a live reference to the same memory, so there’s nothing to race on. But it also means every exchange of data is a full copy (or an ownership handoff), which is wasteful for workloads that need multiple threads actively reading and writing the same large buffer — real-time audio processing, physics simulations, or shared state in a multi-threaded WebAssembly module.

SharedArrayBuffer breaks that isolation deliberately: the same memory is genuinely accessible from both the main thread and any worker it’s been passed to, at the same time.

Why you need Atomics once memory is shared

The moment two threads can touch the same bytes concurrently, ordinary reads and writes stop being safe. A plain array[i]++ involves a read, an increment, and a write — three separate steps — and if another thread modifies array[i] in between, the update is lost. This is the same class of race condition that any concurrent system has to deal with, just now inside JavaScript.

Atomics provides operations that complete as a single, indivisible step from every other thread’s perspective:

const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);

// Atomic increment: read-modify-write as one indivisible operation
Atomics.add(view, 0, 1);

// Compare-and-swap: only write if the current value matches expectations
Atomics.compareExchange(view, 0, oldValue, newValue);

// Block a worker thread until a value changes — not usable on the main thread
Atomics.wait(view, 0, oldValue);
Atomics.notify(view, 0);

Atomics.wait and Atomics.notify add real thread-coordination primitives — a worker can block until another thread signals a change, which is the building block for locks, semaphores, and other synchronization structures that message-passing concurrency can’t express directly. Atomics.wait can only be called from a worker, never the main thread, since blocking the main thread would freeze the page.

The security reason this API is locked down

SharedArrayBuffer was briefly disabled across major browsers after the Spectre speculative-execution vulnerabilities were disclosed, because high-resolution shared memory combined with precise timers made certain side-channel timing attacks significantly easier to pull off. It’s back today, but gated behind cross-origin isolation: a page must opt in by serving the right Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy response headers, which guarantee the page’s browsing context is isolated from other origins before the browser grants it access to genuinely shared, high-precision memory. If those headers aren’t set, SharedArrayBuffer simply isn’t available in that page’s global scope, even though the class exists in the language.

Where this actually gets used

Very little day-to-day web development touches this API directly. It shows up in performance-critical, compute-heavy code: multi-threaded WebAssembly builds that need shared linear memory across worker threads (see WebAssembly for the broader picture), real-time audio or video processing pipelines that split work across workers without copying large buffers on every frame, and libraries implementing higher-level concurrency primitives — mutexes, semaphores, thread pools — on top of Atomics.wait/notify. Most application code that just needs to offload work to a worker is better served by ordinary postMessage with transferable objects; shared memory earns its complexity only when the copy or handoff cost of message passing is itself the bottleneck.

The takeaway

SharedArrayBuffer gives JavaScript threads genuinely shared memory instead of the copy-or-transfer model postMessage normally enforces, and Atomics provides the race-free operations — including blocking wait/notify — needed to coordinate access to it safely. It’s gated behind cross-origin isolation headers for good security reasons, and it’s a narrow, specialized tool: reach for it only when a workload’s performance genuinely depends on multiple threads touching the same memory, not as a general replacement for worker message passing.

Takina Takina · · 6 min read

How to Break Up Long Tasks in JavaScript

Long tasks block the main thread for 50ms or more and make pages feel frozen. How to split them with yielding, scheduler.yield(), postTask, and workers.

#JavaScript #Performance #Web Development
Takina Takina · · 5 min read

Microtasks vs Macrotasks in JavaScript, Explained

Microtasks (promise callbacks) run before the next macrotask (timers, events). How the two queues are ordered, and why it matters for your code.

#JavaScript #Web Development #Performance
Takina Takina · · 5 min read

OffscreenCanvas API Explained

OffscreenCanvas lets you render canvas graphics off the main thread, in a web worker, so heavy drawing work stops blocking scrolling, input, and animation.

#JavaScript #Web Development #Performance