Real-Time Cross-Tab Sync with SharedArrayBuffer and Atomics (No Server, No BroadcastChannel)
Most web devs sync browser tabs using localStorage or BroadcastChannel. But what if you need instant, high-frequency data sharing between tabs — like a multiplayer editor or live data dashboard? Turns out, you can use SharedArrayBuffer + Atomics to achieve true shared memory between tabs — like low-level multi-threading in JavaScript. It’s crazy fast and doesn't need a server. Let’s build a blazing-fast, zero-latency cross-tab bus using shared memory. Step 1: Enable Cross-Origin Isolation (Required) For security, SharedArrayBuffer is only available in cross-origin isolated contexts. You’ll need to serve your site with special headers: Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp If you're using Vite, add this to vite.config.ts: server: { headers: { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp" } } Now SharedArrayBuffer is enabled. Step 2: Create Shared Memory Buffer Each tab will access the same memory buffer using postMessage with transferable ownership. const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 1024); const sharedArray = new Int32Array(sharedBuffer); You can use this like a raw memory segment. But how do other tabs get access to the same buffer? Step 3: Share Memory Between Tabs via window.open Unfortunately, different tabs can’t share memory automatically — but you can pass the buffer using postMessage and the window.open() reference. Tab A (main window): const child = window.open("/child.html"); child.addEventListener("load", () => { child.postMessage(sharedBuffer, "*", [sharedBuffer]); }); Tab B (child window): window.addEventListener("message", (e) => { const shared = new Int32Array(e.data); startWatching(shared); }); You now have shared memory between tabs. Step 4: Use Atomics to Sync Writes Use Atomics to coordinate read/write access and notify changes: // Writer (in Tab A) Atomics.store(sharedArray, 0, Date.now()); // write a timestamp Atomics.notify(sharedArray, 0); // notify any listener // Reader (in Tab B) function startWatching(shared) { function waitLoop() { Atomics.wait(shared, 0, 0); // wait until value changes const timestamp = Atomics.load(shared, 0); console.log("New data received:", new Date(timestamp)); waitLoop(); } waitLoop(); } This loop blocks (in a worker or idle time) until a change is written. It's fast — and zero-polling. Step 5: Add a Messaging Protocol You can extend the buffer to hold structured messages — like IDs, types, or payloads. // Convention: // Index 0: change flag // Index 1: message type // Index 2..: payload // Writer Atomics.store(sharedArray, 1, 42); // message type Atomics.store(sharedArray, 2, 1234); // payload Atomics.store(sharedArray, 0, 1); // signal change Atomics.notify(sharedArray, 0); You now have a structured event bus shared between browser tabs — no server or DB required. ✅ Pros: ⚡ Blazing-fast memory sharing across tabs
Most web devs sync browser tabs using localStorage
or BroadcastChannel
. But what if you need instant, high-frequency data sharing between tabs — like a multiplayer editor or live data dashboard?
Turns out, you can use SharedArrayBuffer
+ Atomics
to achieve true shared memory between tabs — like low-level multi-threading in JavaScript. It’s crazy fast and doesn't need a server.
Let’s build a blazing-fast, zero-latency cross-tab bus using shared memory.
Step 1: Enable Cross-Origin Isolation (Required)
For security, SharedArrayBuffer
is only available in cross-origin isolated contexts. You’ll need to serve your site with special headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
If you're using Vite, add this to vite.config.ts
:
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp"
}
}
Now SharedArrayBuffer
is enabled.
Step 2: Create Shared Memory Buffer
Each tab will access the same memory buffer using postMessage
with transferable ownership.
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 1024);
const sharedArray = new Int32Array(sharedBuffer);
You can use this like a raw memory segment. But how do other tabs get access to the same buffer?
Step 3: Share Memory Between Tabs via window.open
Unfortunately, different tabs can’t share memory automatically — but you can pass the buffer using postMessage
and the window.open()
reference.
Tab A (main window):
const child = window.open("/child.html");
child.addEventListener("load", () => {
child.postMessage(sharedBuffer, "*", [sharedBuffer]);
});
Tab B (child window):
window.addEventListener("message", (e) => {
const shared = new Int32Array(e.data);
startWatching(shared);
});
You now have shared memory between tabs.
Step 4: Use Atomics to Sync Writes
Use Atomics
to coordinate read/write access and notify changes:
// Writer (in Tab A)
Atomics.store(sharedArray, 0, Date.now()); // write a timestamp
Atomics.notify(sharedArray, 0); // notify any listener
// Reader (in Tab B)
function startWatching(shared) {
function waitLoop() {
Atomics.wait(shared, 0, 0); // wait until value changes
const timestamp = Atomics.load(shared, 0);
console.log("New data received:", new Date(timestamp));
waitLoop();
}
waitLoop();
}
This loop blocks (in a worker or idle time) until a change is written. It's fast — and zero-polling.
Step 5: Add a Messaging Protocol
You can extend the buffer to hold structured messages — like IDs, types, or payloads.
// Convention:
// Index 0: change flag
// Index 1: message type
// Index 2..: payload
// Writer
Atomics.store(sharedArray, 1, 42); // message type
Atomics.store(sharedArray, 2, 1234); // payload
Atomics.store(sharedArray, 0, 1); // signal change
Atomics.notify(sharedArray, 0);
You now have a structured event bus shared between browser tabs — no server or DB required.
✅ Pros:
- ⚡ Blazing-fast memory sharing across tabs