JavaScript SharedWorker Table

PieceWhat it doesField note
new SharedWorker(script)The shared instancesame-origin script, ONE worker for every tab of the origin - ten tabs, one execution
worker.portThe tab channela MessagePort per tab; postMessage through it, start() explicitly if handlers attach late
onconnectThe registry pointfires per arriving tab with e.ports[0] - keep ports in a Map; this registry is your router table
MessagePort routingThe muxthe API delivers ports, your code routes messages - broadcast, directed replies, tab-to-tab chat are all yours
die-on-last-tabThe lifecycle truthno persistence promise: last tab closes, worker dies, next session cold-boots - rebuild from IndexedDB every boot
tab identityThe DIY protocolno built-in ids: handshake in onconnect, stamp messages, heartbeat for liveness, leader election on top
same-origin + file://The hard wallopaque origins throw in the constructor; all tabs must share protocol/host/port
support matrixThe Safari latenessChromium/Firefox old, Safari only since 16 (2022) - feature-detect and keep a per-tab Worker + BroadcastChannel fallback
Reference: the MDN SharedWorker. The whole point is ONCE: one WebSocket, one hot cache, one coordinator instead of N tabs times N connections - the API ships port delivery, and your protocol ships identity, routing and liveness.
Bottom line: design for the death. The worker dies with the last tab and cold-boots without memory, so in-memory state is a cache and boot is a deterministic rebuild from storage. Choose by power: storage events for read-only announcements, BroadcastChannel for pure pub-sub, SharedWorker when shared live code and state must exist exactly once, ServiceWorker when it must outlive all tabs - and keep the fallback path alive for the decade of Safari that shipped none of this.
Related tools: the web workers table (the per-tab dedicated sibling), the broadcast channel table (the messaging alternative), the web locks table (coordination without a worker), the localStorage table (the storage-event newspaper), and the websocket table (the resource worth sharing once).

A SharedWorker is one worker instance shared by every tab of your origin: ten open tabs, one script execution, one set of connections. Each tab gets a MessagePort (worker.port) to talk through; the worker sees a connect event per arriving tab and keeps the ports - which makes it the natural home for anything that must exist exactly once: a single WebSocket to the server, a cache hot in memory, a shared undo stack, coordination that localStorage events and BroadcastChannel cannot hold because they carry no shared live state.

Bottom line: the API delivers ports; the architecture is yours. There is no built-in tab identity, no broadcast primitive, no persistence promise - you keep a registry of ports inside onconnect, stamp your own message protocol (tab ids, join/leave notices), and route messages between ports in your own code. The worker also has no lease on life: when the last tab closes, the worker dies - and when the first tab of a new session opens, a fresh instance cold-boots. Every boot must therefore rebuild state from storage (IndexedDB) as if nothing survived, because nothing did.

The honest part is support history: Firefox and Chromium shipped SharedWorker years ago, but Safari refused it for about a decade before finally landing it in 16 (2022) - an entire generation of multi-tab apps grew up on BroadcastChannel and storage-event workarounds because of that hole. The professional posture is unchanged: feature-detect, and keep a fallback where every tab runs its own regular Worker plus a sync channel; shared-instance is an optimization, not an architecture you can require.

How to use

  1. Connect from a tab: const w = new SharedWorker('shared.js'); w.port.postMessage({type: 'hello'}); w.port.onmessage = e => applyUpdate(e.data); - same-origin script only; every tab running this line gets a port to the SAME worker instance.
  2. Register on connect: onconnect = e => { const port = e.ports[0]; port.onmessage = ev => route(ev.data, port); port.postMessage({type: 'welcome', id: nextId()}); } - the connect event fires once per tab; keep the port in a Map if anyone needs to broadcast later.
  3. Route between tabs: with the port registry, tab-to-tab is your code - ports.forEach(p => p.postMessage(msg)) broadcasts; directed replies go to the originating port. start() explicitly if you attach handlers late.
  4. Hold the single resource: open the WebSocket / IndexedDB / audio engine inside the worker, fan out server events to all ports, and dedupe - N tabs, one socket, one quota of connections; tabs post intents, the worker owns the resource.
  5. Boot idempotently and detect support: on every boot, rebuild in-memory state from IndexedDB before serving ports (the worker may be a cold start); if (!('SharedWorker' in window)) fall back to a per-tab Worker plus BroadcastChannel sync.

Frequently asked questions

When exactly does a SharedWorker die, and what happens to my state?

It lives exactly as long as its connections: when the last tab holding a port closes (or is killed by the OS), the worker terminates - state, sockets, everything - and there is no warning event worth designing around. A new session cold-boots a fresh instance from the script file. The engineering consequence: in-memory state is a cache, never the source of truth. Write-through to IndexedDB as state changes, and make boot a deterministic rebuild: read storage, re-establish the WebSocket, resync with the server, then accept tab connections (or queue connect events while booting - ports message you before you are ready, so buffer early messages). If something must outlive all tabs - push receiving, background sync - that is a service worker's job, not a SharedWorker's; the two are complements: SharedWorker for live shared state while tabs exist, SW for existence without tabs.

How do I tell tabs apart when the API gives me no tab identity?

You mint it yourself in the connect protocol. The worker assigns each arriving port an incrementing id (or accepts the tab's self-declared name), and both sides stamp every message with that id; a hello/welcome handshake over the port settles metadata (tab title, role, whether it is the leader). Two patterns fall out: leader election for tasks only one tab may run (the worker simply picks the first connected port and re-elects on its disconnect), and join/leave presence - broadcast a roster to all ports whenever the connect event fires or a port's message loop goes silent (MessagePort has no close notification you can rely on, so a heartbeat ping every few seconds is the honest liveness signal). This is exactly the protocol layer people expect the API to include; it does not - the API is a port delivery service, the rest is your ten lines.

SharedWorker vs BroadcastChannel vs storage events - which do I use?

They solve overlapping problems at different power levels. storage events are a read-only newspaper: every other tab learns a localStorage key changed - fine for theme toggles, useless for conversation (the writing tab gets no event) and for anything richer than key-values. BroadcastChannel is a real pub-sub channel: every tab subscribes, every tab receives (including semantics you control) - the right default for pure messaging like cross-tab logout or cache invalidation, and it needs no worker file. SharedWorker is the only one of the three that hosts code and state in one place - a single socket, a single in-memory cache, leader-election logic - which messaging alone cannot express. The decision rule: announce facts to tabs = BroadcastChannel; own a shared live resource with routing = SharedWorker; and on engines without SharedWorker (Safari pre-16), BroadcastChannel plus per-tab workers is the accepted degradation.

How do I debug a SharedWorker, and what are the common silent failures?

The worker's console does not appear in any tab's DevTools - attach by URL: Chrome/Edge expose it under chrome://inspect (or DevTools Sources - dedicated workers list as shared once connected), Firefox under about:debugging. The classic silent failures: same-origin violations (the constructor throws - check protocol/host/port, and beware file:// where origins are opaque), a missing port.start() when handlers were attached after the first message, buffering misses (a tab posts before the worker finished booting - buffer early messages), and the pre-16 Safari hole where typeof SharedWorker is undefined and code assuming it dies quietly. One more: the worker script is fetched once per session and cached per HTTP rules - a stale worker file serving ten tabs is a versioning problem; give the worker a version ping so tabs can detect mismatched protocols and reload themselves.

Related tools