JavaScript SharedWorker Table
| Piece | What it does | Field note |
|---|---|---|
new SharedWorker(script) | The shared instance | same-origin script, ONE worker for every tab of the origin - ten tabs, one execution |
worker.port | The tab channel | a MessagePort per tab; postMessage through it, start() explicitly if handlers attach late |
onconnect | The registry point | fires per arriving tab with e.ports[0] - keep ports in a Map; this registry is your router table |
MessagePort routing | The mux | the API delivers ports, your code routes messages - broadcast, directed replies, tab-to-tab chat are all yours |
die-on-last-tab | The lifecycle truth | no persistence promise: last tab closes, worker dies, next session cold-boots - rebuild from IndexedDB every boot |
tab identity | The DIY protocol | no built-in ids: handshake in onconnect, stamp messages, heartbeat for liveness, leader election on top |
same-origin + file:// | The hard wall | opaque origins throw in the constructor; all tabs must share protocol/host/port |
support matrix | The Safari lateness | Chromium/Firefox old, Safari only since 16 (2022) - feature-detect and keep a per-tab Worker + BroadcastChannel fallback |
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
- 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.
- 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.
- 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.
- 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.
- 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.