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.
- It stores bytes, not JavaScript objects.
- It is commonly accessed through typed array views such as Int32Array or Uint8Array.
- It is designed for concurrent access across workers or other agent contexts.
- It does not provide automatic locking or conflict resolution.
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:
- Fast communication between a main thread and workers.
- Shared access to large binary data like audio, video, or simulation buffers.
- Low-latency coordination between producers and consumers.
- Efficient implementations of ring buffers, queues, or real-time pipelines.
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
- Streaming audio data from one worker to another with a shared ring buffer.
- Tracking simulation state in physics or game engines without repeated copying.
- Passing image or video frame metadata between a producer and consumer worker.
- Maintaining counters, indexes, or flags for background processing.
- Coordinating parsers, decoders, or data pipelines where latency matters.
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
- SharedArrayBuffer is not a general replacement for ordinary arrays or objects.
- It is byte-based, so you must encode text and structured data yourself.
- Atomic operations are limited to specific typed arrays and integer operations.
- Browser availability depends on cross-origin isolation and a secure context.
- Race conditions can still happen if you read and write shared data without coordination.
- Debugging can be harder because bugs may only appear under concurrency or load.
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
- SharedArrayBuffer gives multiple JavaScript agents access to the same memory.
- It is best for performance-sensitive shared data and worker communication.
- Typed arrays are the normal way to read and write the shared bytes.
- Atomics are essential when more than one agent can write the same location.
- Browser support depends on security requirements such as cross-origin isolation.
11. Practice Exercise
- Create a shared buffer with 4 bytes.
- Wrap it in an Int32Array.
- Store the number 10 in index 0.
- Use Atomics.add to increase it by 5.
- Print the final value.
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.