JavaScript SharedArrayBuffer: Shared Memory for Worker Concurrency

SharedArrayBuffer lets multiple JavaScript agents, especially workers, read and write the same block of memory without copying it. That makes it useful for high-performance concurrency, but it also requires careful synchronization and the right browser security settings.

Quick answer: SharedArrayBuffer creates shared memory, not a copied message payload. Use it when you need low-copy communication between workers, and pair it with Atomics when multiple threads can update the same data.

Difficulty: Advanced

You'll understand this better if you know: JavaScript arrays and typed arrays, how Web Workers communicate with postMessage, and the basic idea of shared versus copied data.

1. What Is SharedArrayBuffer?

SharedArrayBuffer is a built-in JavaScript object that represents raw binary memory shared across execution contexts. Unlike a normal ArrayBuffer, a shared buffer is not detached or transferred when sent to another worker; every participant sees the same underlying bytes.

In practice, SharedArrayBuffer is the memory layer, and Atomics is the coordination layer that helps avoid race conditions.

2. Why SharedArrayBuffer Matters

Most JavaScript data exchange between threads involves copying or transferring values. That is fine for many apps, but it can become expensive for large datasets, frequent updates, or low-latency communication. Shared memory avoids repeated copies and can significantly reduce overhead.

You would use SharedArrayBuffer when you need one or more of these benefits:

You usually would not use it for ordinary application state, JSON data, or small messages, because the complexity of synchronization is not worth it.

3. Basic Syntax or Core Idea

Create a shared buffer

The constructor looks similar to ArrayBuffer, but the result can be shared rather than transferred.

const buffer = new SharedArrayBuffer(1024);

This creates 1,024 bytes of shared memory.

Create typed array views

You normally read and write the buffer through a typed array view.

const bytes = new Uint8Array(buffer);

The view does not copy the data. It gives you indexed access to the shared bytes.

Use Atomics when multiple agents write

For synchronization, use Atomics on integer typed arrays such as Int32Array.

const state = new Int32Array(buffer);

Atomic operations work on shared integer views and help keep updates consistent across threads.

4. Step-by-Step Examples

Example 1: Sharing a buffer with a worker

This example creates a shared buffer in the main thread and sends it to a worker. Both sides can read and write the same memory.

const buffer = new SharedArrayBuffer(16);const view = new Uint8Array(buffer);view[0] = 42;const worker = new Worker("worker.js");worker.postMessage({ buffer });

In the worker, the same buffer can be wrapped in another typed array view and read immediately. No data copy is needed.

Example 2: Incrementing a shared counter safely

When more than one worker may update the same number, plain reads and writes can collide. Here, Atomics.add updates a shared counter safely.

const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);const counter = new Int32Array(buffer);Atomics.add(counter, 0, 1);Atomics.add(counter, 0, 1);

Because the increments are atomic, concurrent updates do not overwrite each other.

Example 3: Using the buffer as a small message header

A shared buffer is often split into fields. For example, the first few bytes can store status flags or a write index.

const buffer = new SharedArrayBuffer(8);const state = new Int32Array(buffer);Atomics.store(state, 0, 1);Atomics.store(state, 1, 128);

This pattern is useful for publishing a small amount of control data alongside a larger raw data region.

Example 4: Waiting for work to be ready

Workers can coordinate by waiting on a shared integer value and waking each other after the state changes.

const buffer = new SharedArrayBuffer(4);const flag = new Int32Array(buffer);Atomics.store(flag, 0, 0);// Later, another worker can wait until this value changes.

In real worker code, Atomics.wait and Atomics.notify are used together, but only certain environments allow blocking waits.

5. Practical Use Cases

These use cases all benefit from a shared memory model where many small updates happen frequently.

6. Common Mistakes

Mistake 1: Treating SharedArrayBuffer like a normal object store

SharedArrayBuffer only stores bytes. New developers sometimes expect to save strings, arrays, or objects directly inside it.

Problem: A shared buffer cannot hold JavaScript objects, so writing data like it were a normal array does not work.

const buffer = new SharedArrayBuffer(8);buffer[0] = "hello";

Fix: Use a typed array and store bytes or numeric encodings.

const buffer = new SharedArrayBuffer(8);const bytes = new Uint8Array(buffer);bytes[0] = 104;

The corrected version works because typed arrays provide the actual read and write interface.

Mistake 2: Using non-atomic writes for shared counters

When two workers update the same value, a normal increment can lose updates because each agent may read the old value before either write completes.

Problem: A plain increment on shared state can cause race conditions and incorrect results.

const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);const count = new Int32Array(buffer);count[0] = count[0] + 1;

Fix: Use an atomic operation so the read-modify-write happens as one coordinated step.

const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);const count = new Int32Array(buffer);Atomics.add(count, 0, 1);

The corrected version works because Atomics.add prevents lost updates.

Mistake 3: Assuming SharedArrayBuffer works in every browser context

Modern browsers restrict shared memory to pages that are cross-origin isolated. If the page is not configured correctly, the constructor may be unavailable or fail to behave as expected.

Problem: Browsers can block SharedArrayBuffer outside a secure, cross-origin isolated context, leading to SharedArrayBuffer is not defined or similar availability issues.

const buffer = new SharedArrayBuffer(1024);

Fix: Serve the page over HTTPS and configure cross-origin isolation headers so the browser allows shared memory.

const supported = typeof SharedArrayBuffer !== "undefined";

The corrected approach checks availability first, and the environment must also be configured properly before shared memory can be used.

7. Best Practices

Practice 1: Reserve shared memory for hot paths

Use shared memory only when copying becomes a real bottleneck. For one-off messages, the extra synchronization cost is often worse than a normal data transfer.

const buffer = new SharedArrayBuffer(4096);const bytes = new Uint8Array(buffer);

This keeps the shared region focused on high-frequency data rather than general-purpose state.

Practice 2: Use integer views for coordination

Shared state that controls ordering should use Int32Array or another supported integer view, because atomic operations are defined on integer typed arrays.

const control = new Int32Array(new SharedArrayBuffer(16));Atomics.store(control, 0, 1);

That separation makes the code clearer and avoids trying to use atomic APIs on incompatible views.

Practice 3: Document the memory layout

When multiple workers share the same buffer, define which byte ranges mean what. Treat the buffer like a protocol rather than a loose collection of bytes.

const buffer = new SharedArrayBuffer(64);const header = new Int32Array(buffer, 0, 4);const payload = new Uint8Array(buffer, 16);

Explicit layout rules reduce bugs and make worker code easier to maintain.

8. Limitations and Edge Cases

If a shared value appears to change unpredictably, the issue is often not the buffer itself but the lack of a clear synchronization strategy.

9. Practical Mini Project

Here is a small shared counter design you could use as the core of a task tracker between the main thread and one worker. The main thread owns the buffer and the worker increments the counter whenever work is completed.

const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);const tasksDone = new Int32Array(buffer);Atomics.store(tasksDone, 0, 0);function markTaskComplete() {  Atomics.add(tasksDone, 0, 1);} markTaskComplete();markTaskComplete();console.log(Atomics.load(tasksDone, 0));

This simple setup shows the core pattern: create shared memory, wrap it in a typed array, and use atomic operations to update shared state safely.

10. Key Points

11. Practice Exercise

Expected output: 15

Hint: Use Atomics.store first, then Atomics.add, and read the result with Atomics.load.

Solution:

const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);const value = new Int32Array(buffer);Atomics.store(value, 0, 10);Atomics.add(value, 0, 5);console.log(Atomics.load(value, 0));

12. Final Summary

SharedArrayBuffer is the JavaScript primitive for shared memory. It allows different workers or agents to operate on the same bytes without copying data, which can improve performance in real-time and high-throughput scenarios.

The tradeoff is complexity: shared memory requires typed arrays, careful layout, and atomic coordination to avoid race conditions. In browsers, you also need the right security configuration before shared memory is available.

If you are building worker-based pipelines, simulations, or other performance-sensitive systems, the next step is to learn Atomics in detail and then study practical worker communication patterns such as ring buffers and producer-consumer queues.