The `SharedArrayBuffer` in TypeScript
This article explains the SharedArrayBuffer in TypeScript.
We will explain SharedArrayBuffer in TypeScript with practical examples.
YouTube Video
The SharedArrayBuffer in TypeScript
SharedArrayBuffer is a mechanism for sharing the same memory space between multiple threads, such as Web Workers. By combining it with Atomics, you can manage data races and perform low-latency shared memory operations.
Prerequisites and Notes
When using SharedArrayBuffer in the browser, COOP and COEP headers are required to meet security requirements known as cross-origin isolation. In Node.js, shared memory can be handled relatively easily using worker_threads.
Basic Concepts
SharedArrayBuffer is an object representing a fixed-length sequence of bytes, and you can read and write numbers via TypedArray and similar views. Simple read/write operations are not synchronized, so you use the Atomics API to ensure atomic operations and use wait and notify mechanisms for coordination.
Simple Counter (Browser version)
In this example, the main thread creates a SharedArrayBuffer and passes it to a Web Worker, with both incrementing a shared counter. This demonstrates the minimal pattern: atomic addition with Atomics.add and reading with Atomics.load.
main.ts (Browser side)
This example shows how the main thread creates a SharedArrayBuffer and shares it with a Worker for multi-threaded access.
1// main.ts
2// Create a 4-byte (Int32) shared buffer for one counter
3const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 1);
4const counter = new Int32Array(sab); // view over the buffer
5
6// Create worker and transfer the SharedArrayBuffer
7const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
8worker.postMessage({ sab });
9
10// Increase counter in main thread every 200ms
11setInterval(() => {
12 const newVal = Atomics.add(counter, 0, 1) + 1; // Atomically increment
13 console.log('Main incremented ->', newVal);
14}, 200);
15
16// Listen for messages (optional)
17worker.onmessage = (e) => {
18 console.log('From worker:', e.data);
19};- In this code, the main thread uses
Atomics.addto increment the value atomically. On theworker.tsside, the sameSharedArrayBuffercan be accessed and manipulated.
worker.ts (Browser Worker)
This is an example where the worker receives the same shared buffer and periodically decrements or manipulates it.
1// worker.ts
2self.onmessage = (ev: MessageEvent) => {
3 const { sab } = ev.data as { sab: SharedArrayBuffer };
4 const counter = new Int32Array(sab);
5
6 // Every 350ms, atomically add 2 (demonstration)
7 setInterval(() => {
8 const newVal = Atomics.add(counter, 0, 2) + 2;
9 // Optionally notify main thread (postMessage)
10 self.postMessage(`Worker added 2 -> ${newVal}`);
11 }, 350);
12};- The Worker also manipulates the same memory through
Int32Array, and updates are made without race conditions thanks toAtomics.
Synchronization using wait/notify
By using Atomics.wait and Atomics.notify, you can suspend threads until certain conditions are met, enabling event-driven synchronization within worker threads. In browsers, the safest approach is to use Atomics.wait inside a Worker.
producer.ts (Browser Producer Worker)
The producer writes data and notifies the consumer using notify.
1// producer.ts (worker)
2self.onmessage = (ev: MessageEvent) => {
3 const { sab } = ev.data as { sab: SharedArrayBuffer };
4 const state = new Int32Array(sab); // state[0] will be 0=empty,1=filled
5
6 // produce every 500ms
7 setInterval(() => {
8 // produce: set state to 1 (filled)
9 Atomics.store(state, 0, 1);
10 Atomics.notify(state, 0, 1); // wake one waiter
11 self.postMessage('produced');
12 }, 500);
13};consumer.ts (Browser Consumer Worker)
The consumer waits with Atomics.wait and resumes processing upon notification from the producer.
1// consumer.ts (worker)
2self.onmessage = (ev: MessageEvent) => {
3 const { sab } = ev.data as { sab: SharedArrayBuffer };
4 const state = new Int32Array(sab);
5
6 // consumer loop
7 (async function consumeLoop() {
8 while (true) {
9 // if state is 0 => wait until notified (value changes)
10 while (Atomics.load(state, 0) === 0) {
11 // Blocks this worker until notified or timeout
12 Atomics.wait(state, 0, 0);
13 }
14 // consume (reset to 0)
15 // (processing simulated)
16 // read data from another shared buffer in real scenarios
17 Atomics.store(state, 0, 0);
18 // continue loop
19 self.postMessage('consumed');
20 }
21 })();
22};- In this pattern, the producer notifies the consumer via
Atomics.notifyand the consumer waits efficiently usingAtomics.wait.Atomics.waitcannot be called on the main thread in browsers. To prevent the UI from freezing,Atomics.waitis restricted to use only within Workers.
Practical Example with Node.js (worker_threads)
In a Node.js environment, you can handle SharedArrayBuffer functionality using worker_threads. Below is a typed TypeScript sample for Node.js.
main-node.ts
The main thread creates the buffer and passes it to the Worker.
1// main-node.ts
2import { Worker } from 'worker_threads';
3import path from 'path';
4
5// Create SharedArrayBuffer for one 32-bit integer
6const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
7const counter = new Int32Array(sab);
8
9// Spawn worker and pass sab via workerData
10const worker = new Worker(path.resolve(__dirname, 'worker-node.js'), {
11 workerData: { sab }
12});
13
14// Log messages from worker
15worker.on('message', (msg) => console.log('Worker:', msg));
16
17// Increment in main thread periodically
18setInterval(() => {
19 const val = Atomics.add(counter, 0, 1) + 1;
20 console.log('Main increment ->', val);
21}, 300);worker-node.ts
This example uses parentPort and workerData from worker_threads on the Worker side.
1// worker-node.ts
2import { parentPort, workerData } from 'worker_threads';
3
4const sab = workerData.sab as SharedArrayBuffer;
5const counter = new Int32Array(sab);
6
7// Periodically add 5
8setInterval(() => {
9 const val = Atomics.add(counter, 0, 5) + 5;
10 parentPort?.postMessage(`Added 5 -> ${val}`);
11}, 700);- In Node.js, there are no COOP or COEP restrictions like in browsers, so shared memory can be easily handled just by using
worker_threads. When using TypeScript, pay attention to whether you are building with CommonJS or ESM settings.
TypeScript Typing Points
SharedArrayBuffer and Atomics are included in the standard DOM and library type definitions, so you can use them directly in TypeScript. When exchanging messages with Workers, it is safer to define interfaces and specify types clearly.
1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };- Explicitly defining types makes
postMessageandonmessagehandling safer and enables type checking.
Practical Use Cases
SharedArrayBuffer is not always necessary, but it is highly effective in situations where the speed advantages of shared memory are required. Understanding the situations where it is effective leads to appropriate technology choices.
- It is suitable for low-latency processing that requires high-speed shared buffers and can be used for real-time audio/video processing or game physics.
- For simple data exchange or transferring large amounts of data,
Transferable ArrayBufferorpostMessagemay be easier to use thanSharedArrayBuffer.
Constraints and Security
To use SharedArrayBuffer in the browser, cross-origin isolation is required: set COOP to same-origin-allow-popups and COEP to require-corp. SharedArrayBuffer will be disabled unless these requirements are met.
Performance and Debugging Tips
Atomic operations (Atomics) are fast, but frequent waiting and excessive synchronization can increase latency.
The following points should be checked to handle shared memory safely and efficiently.
- Views like
Int32Arrayshould be handled with proper byte alignment. - Be clear about which indices of the same shared buffer are used by which processes, and maintain consistent conventions in your code.
- If you treat a
SharedArrayBufferlike a normalArrayBuffer, data races will occur. Writing to the same index from multiple threads without usingAtomicsis dangerous.
Summary
By making proper use of SharedArrayBuffer and Atomics, you can achieve safe and fast shared memory operations even in TypeScript. Starting with simple synchronization patterns like counters or signals is easier to understand, and by thoroughly managing correct synchronization and indices, you can be effective even in low-latency scenarios.
You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.