Open your app in five tabs and, by default, you get five WebSocket connections, five polling loops, and five copies of the same in-memory cache. Each tab is an island. The server sees five clients where there is really one user, and your fan-out cost scales with the number of tabs a person happens to leave open — not the number of people using the product.
SharedWorker fixes that, and as of the May 2026 Baseline digest, published June 3, it is finally Baseline Newly available — supported in the current stable releases of Chrome, Edge, Firefox, and Safari. The API itself is old; the news is that the last engine gaps closed, so you can reach for it without a per-browser fallback for the common case. A SharedWorker is a single background script that every same-origin tab, window, and iframe can connect to. One thread, one connection, one source of truth, shared across all of them.
Dedicated workers vs. shared workers
A regular Worker is scoped to the page that created it. Close that page and the worker dies; open the page twice and you get two independent workers. That is the right model for CPU-bound work you want to parallelize per tab — parsing, image processing, anything you'd offload from the main thread.
A SharedWorker is scoped to the origin, not the page. The first tab to request one spins it up; every tab after that connects to the same running instance. It keeps living as long as at least one tab holds a connection to it. This makes it the natural home for anything that should exist once per user rather than once per tab: a realtime connection, a shared cache, a coordination point between tabs.
The trade-off is the communication model. A dedicated worker talks to its one page directly through postMessage. A shared worker can have many pages connected at once, so it talks through ports — one MessagePort per connection — and your worker code has to manage the set of connected ports itself.
The minimal wiring
On the page, you instantiate the worker and talk to it through .port:
// main.js (runs in each tab)
const worker = new SharedWorker('/shared-worker.js');
worker.port.start();
worker.port.postMessage({ type: 'subscribe', channel: 'prices' });
worker.port.addEventListener('message', (event) => {
console.log('from shared worker:', event.data);
});
Two things are easy to miss. First, when you attach a listener with addEventListener (rather than assigning port.onmessage), you must call port.start() explicitly to open the port — onmessage starts it implicitly, addEventListener does not. Second, every tab gets its own port, but they all reach the same worker.
Inside the worker, the entry point is the connect event. It fires once per connecting tab, and event.ports[0] is that tab's end of the channel:
// shared-worker.js
const ports = new Set();
self.addEventListener('connect', (event) => {
const port = event.ports[0];
ports.add(port);
port.start();
port.addEventListener('message', (e) => {
if (e.data.type === 'subscribe') {
// handle subscription for this tab
}
});
});
// Broadcast to every connected tab
function broadcast(data) {
for (const port of ports) {
port.postMessage(data);
}
}
That ports set is the whole pattern: the worker keeps a list of who's connected and fans messages out to all of them. This is documented on the MDN SharedWorker reference, which is the canonical spec-level description of the API.
The payoff case: one WebSocket for the whole app
The pattern that justifies the API is sharing a single realtime connection. Instead of each tab opening its own WebSocket, the SharedWorker owns exactly one, and every tab subscribes to it through its port. The server sees one client per user instead of one per tab, which is the difference between predictable fan-out cost and cost that scales with how messy your users' tab habits are. This cross-tab WebSocket approach is the classic use case, well covered in this dev.to write-up on scaling WebSocket connections with shared workers.
// shared-worker.js
const ports = new Set();
let socket;
function ensureSocket() {
if (socket && socket.readyState <= WebSocket.OPEN) return;
socket = new WebSocket('wss://api.example.com/stream');
socket.addEventListener('message', (event) => {
for (const port of ports) {
port.postMessage({ type: 'data', payload: event.data });
}
});
socket.addEventListener('close', () => {
// reconnect with backoff if any tab is still listening
if (ports.size > 0) setTimeout(ensureSocket, 1000);
});
}
self.addEventListener('connect', (event) => {
const port = event.ports[0];
ports.add(port);
port.start();
ensureSocket();
});
The wins compound. There is one reconnect-and-backoff state machine instead of one per tab, so your tabs never stampede the server during an outage. Authentication and heartbeat happen once. And because the socket lives in the worker, messages arriving while a tab is backgrounded are still received and can be queued — the connection is not tied to any single page's lifecycle.
Lifecycle: the part that bites people
A SharedWorker lives only as long as something is connected to it. The browser is allowed to terminate the worker once the last port disconnects — and "disconnect" includes the user closing the final tab. That has a direct consequence: any state that must outlive the tabs has to be persisted somewhere durable before the last port goes away. In practice that means writing to IndexedDB or syncing to the server, not trusting the worker's memory to still be there next time.
You also want to clean up ports as tabs close so your broadcast loop doesn't post into dead channels. There is no reliable per-port "closed" event, so the common approach is a heartbeat or an explicit unload message:
// main.js
window.addEventListener('pagehide', () => {
worker.port.postMessage({ type: 'disconnect' });
});
// shared-worker.js — inside the message handler
if (e.data.type === 'disconnect') {
ports.delete(port);
}
It is worth knowing the historical edge: WebKit shipped SharedWorker, removed it years ago, then reinstated it, and support has been solid in modern Safari for a while now — the WebKit tracking bug is the paper trail. The reason this is Baseline news in 2026 is that the matrix across all four engines is now green at once. If your analytics still show meaningful traffic from older Safari or other legacy versions, feature-detect with if ('SharedWorker' in window) and degrade to a per-tab connection. One more caveat: some browsers disable SharedWorker in private or incognito windows, so your fallback path needs to handle a constructor that throws.
When not to reach for it
SharedWorker is not a default — it is a specific answer to a specific problem. If your work is CPU-bound and per-tab, a dedicated Worker is simpler and isolates failure. If you need code that runs even with no tabs open — push notifications, background sync — that is a Service Worker's job, not a SharedWorker's. And if all you need is to tell other tabs "the user just logged out," a BroadcastChannel is a far lighter tool than standing up a shared thread.
The sweet spot is narrow but valuable: persistent, stateful, shared resources that should exist once per user. Realtime feeds, collaborative editing transports, a shared data cache that several tabs read from, a single rate-limited connection to a metered API.
Takeaways
Audit your app for the multi-tab tax. If a user with three tabs open triggers three WebSockets, three polling intervals, or three redundant fetches of the same data, a SharedWorker collapses that to one. Start with the realtime connection — it's the highest-leverage change and the easiest to reason about.
Wire it with the port pattern: the worker keeps a Set of connected ports, fans messages out to all of them, and owns the single shared resource. Remember port.start() when you use addEventListener. Persist anything important to IndexedDB or the server before the last tab closes, because the worker can be terminated the moment nobody is connected. And feature-detect so the long tail of older or private-mode browsers falls back gracefully. Now that it is Baseline across every major engine, the shared-connection pattern is something you can ship to production without an asterisk.
Sources: May 2026 Baseline monthly digest — web.dev · SharedWorker — MDN · Scaling WebSocket Connections using Shared Workers — dev.to