BroadcastChannel vs SharedWorker: Choose
Compare peer broadcast with a shared execution owner across fan-out, late join, missed messages, lifecycle loss, and reconstruction from durable truth.
BroadcastChannel vs SharedWorker is a choice between advisory peer messages and a shared execution owner for same-origin tabs. This comparison tests fan-out, late join, missed delivery, owner loss, and reconstruction while keeping durable truth outside both APIs.
BroadcastChannel vs SharedWorker separates three jobs
Classify the feature before choosing an API. Notification tells peers that something changed. Ownership gives one live execution context responsibility for coordinating mutable work. Persistence lets a new context reconstruct authoritative state after every old context disappears. BroadcastChannel primarily supplies same-origin pub/sub; SharedWorker can supply a shared live owner through connected ports. Neither is durable storage.
The current HTML broadcast-channel section and shared-worker section define the platform contracts. BroadcastChannel vs SharedWorker should not be reduced to a browser-support table because their ownership models differ even where both exist.
The committed fixture simulates tabs as deterministic objects, not actual frozen browsing contexts. It drops messages according to named local states, terminates a synthetic worker, and rebuilds from a small store. Those outcomes explain the decision model without claiming real browser suspension timing or production reliability.
Write one sentence naming the authoritative record for the feature. If the answer is the channel or worker memory, recovery has not been defined and architecture work remains.
- BroadcastChannel fans messages among currently listening peers.
- SharedWorker ports converge on one shared execution context while it lives.
- Neither topology is durable storage.
| Signal | Interpretation |
|---|---|
| Broadcast and worker topology split | Broadcast spokes connect peer tabs while SharedWorker ports converge on a central execution owner. |
Use BroadcastChannel for advisory fan-out
A BroadcastChannel joins contexts by same-origin channel name and delivers posted messages to other listening channel objects according to the standard. It is a clean fit for invalidate-cache, draft-saved, signed-out, preference-changed, or please-refresh hints where each tab already knows how to read current truth.
Messages should carry a version, sender instance ID, event type, and minimal payload or state key. Validate every message even though it is same origin; another version of your application may send an older shape. The BroadcastChannel API does not give a late joiner history, so startup must read durable state before listening for new hints.
Cross-tab messaging is easier to operate when it is disposable. If missing one message corrupts the feature, the message is being used as persistence. BroadcastChannel vs SharedWorker favors broadcast only when peers can tolerate duplication, reordering where applicable, and absence by re-reading the source of truth.
Close channels when a page no longer needs them and avoid high-frequency payload floods. Advisory fan-out should not become an invisible same-origin event firehose.
Use SharedWorker for a live execution owner
A SharedWorker exposes one worker global that multiple same-origin documents can connect to through MessagePort objects. It can hold a websocket, coordinate a rate-limited scheduler, multiplex expensive computation, or serialize an in-memory workflow while participating clients remain connected and the user agent keeps the worker alive.
Define a port handshake with protocol version, client instance, capabilities, and initial state request. Track ports, clean up disconnect assumptions conservatively, and never trust a tab to announce its own closure reliably. SharedWorker state is useful working state, not an authoritative archive. The worker lifetime model must inform recovery design.
BroadcastChannel vs SharedWorker favors the worker when the product truly needs one shared live owner rather than peer notification. Even then, persist checkpoints or reconstructible inputs elsewhere, because user-agent lifecycle can end the owner.
A worker owner also needs fairness among ports. One noisy tab should not monopolize the shared scheduler or starve a foreground action from another client.
Model late join and missed delivery
Open a new tab after several changes. With broadcast, it receives only subsequent messages and must initialize from storage or a server. With a shared worker, it can ask the current owner for a snapshot, but that snapshot should include a durable version so the tab can detect stale or uncommitted state. If no worker survives, connection creates a new owner that reconstructs.
The offline drafts with IndexedDB article is a suitable persistence layer for local drafts, while server authority may be required for account-wide truth. BroadcastChannel vs SharedWorker does not choose the database. It chooses how live contexts learn and coordinate around that database.
Use monotonically increasing application versions or content hashes, not delivery counts, to detect gaps. A tab that sees version 14 after version 11 re-reads state; it does not demand that peers replay two lost payloads. The fixture follows that rule with bounded synthetic versions.
Startup should compare schema versions before applying stored state. A late joiner that understands an older record may need migration or a controlled reload rather than partial recovery.
| Lifecycle | What to assume |
|---|---|
| Active | may receive advisory messages |
| Frozen or closed | may miss them |
| Late join | must reconstruct from durable truth |
| Signal | Interpretation |
|---|---|
| Synthetic tab lifecycle heatmap | Rows for active, frozen, closed, and late-joining tabs intersect delivery and reconstruction columns. |
Design for frozen, closed, and terminated contexts
A background tab may be throttled, frozen, discarded, or closed. A worker may terminate. Do not create promises that assume timely progress in every browsing context. Store leases with expirations, make effects idempotent, and let a resumed tab compare its known version with durable truth before acting.
The lifecycle heatmap is an architectural threat model rather than captured browser evidence. Actual policies vary by browser, device, memory, and power state. Test the supported matrix with deliberate backgrounding and process termination. BroadcastChannel vs SharedWorker should be selected under the assumption that ephemeral contexts can vanish between any two steps.
If exactly one tab must perform a short critical section against shared storage, compare Web Locks coordination rather than inventing leader election in messages. Locks, workers, broadcasts, and persistence each own a different part of the problem.
Test device pressure and background transitions with developer tooling where available, then label those runs by browser version. Synthetic lifecycle panels remain only the deterministic baseline.
The multi-tab session records missed events, terminates its first owner, reconstructs a replacement from its checkpoint, and drains durable work exactly once.
Runnable artifact — multi-tab-coordination-fixture.html
<!doctype html><meta charset="utf-8"><title>Multi-tab coordination fixture</title><button id="run" type="button">Run owner replacement session</button><ol id="tabs"></ol><pre id="events"></pre><output id="receipt"></output><script>
const PROTOCOL=["notification","snapshot_request","snapshot_response","checkpoint"];
const simulate=()=>{const durable={version:0,value:"",checkpoint:null,queue:[],completed:[]},tabs=[{id:"A",state:"active",version:0,events:[]},{id:"B",state:"frozen",version:0,events:[]}],missed=[],protocol=[];let owner={id:"owner-1",state:"active",restoredFrom:null};
const record=(type,detail={})=>{if(!PROTOCOL.includes(type))throw new Error("unknown protocol event");protocol.push({type,...detail})},notify=()=>{record("notification",{version:durable.version});for(const tab of tabs){if(tab.state==="active"){tab.events.push("notification");tab.version=durable.version}else missed.push({tab:tab.id,type:"notification",version:durable.version})}},snapshot=tab=>{record("snapshot_request",{tab:tab.id,owner:owner.id});if(owner.state!=="active")throw new Error("owner unavailable");record("snapshot_response",{tab:tab.id,owner:owner.id,version:durable.version});tab.version=durable.version;tab.value=durable.value;tab.state="reconstructed"},checkpoint=()=>{if(owner.state!=="active")throw new Error("owner unavailable");durable.checkpoint={id:"checkpoint-1",version:durable.version};record("checkpoint",{owner:owner.id,id:durable.checkpoint.id})},enqueue=id=>{if(durable.queue.length===3)throw new Error("bounded queue full");durable.queue.push({id})},drain=current=>{if(current.state!=="active"||current.restoredFrom!==durable.checkpoint.id)throw new Error("owner must reconstruct before drain");for(const task of [...durable.queue])if(!durable.completed.includes(task.id))durable.completed.push(task.id);durable.queue=durable.queue.filter(task=>!durable.completed.includes(task.id));return[...durable.completed]};
durable.version=1;durable.value="draft-1";notify();tabs[1].state="active";snapshot(tabs[1]);const late={id:"C",state:"late",version:0,events:[]};tabs.push(late);snapshot(late);checkpoint();for(const id of ["task-1","task-2","task-3"])enqueue(id);owner.state="terminated";const original={...owner};let blocked=false;try{drain(owner)}catch{blocked=true}owner={id:"owner-2",state:"reconstructing",restoredFrom:null};owner.restoredFrom=durable.checkpoint.id;owner.state="active";const replacement={...owner},firstDrain=drain(owner),secondDrain=drain(owner);const pass=PROTOCOL.every(type=>protocol.some(event=>event.type===type))&&missed.some(event=>event.tab==="B")&&tabs.slice(1).every(tab=>tab.version===durable.version)&&original.state==="terminated"&&replacement.state==="active"&&replacement.restoredFrom==="checkpoint-1"&&blocked&&firstDrain.join(",")==="task-1,task-2,task-3"&&secondDrain.join(",")===firstDrain.join(",")&&durable.queue.length===0&&new Set(durable.completed).size===3;return{PROTOCOL,protocol,missed,tabs,owners:{original,replacement,blockedBeforeReplacement:blocked},durable,firstDrain,secondDrain,pass}};
const runFixture=()=>{const result=simulate();document.querySelector("#tabs").innerHTML=result.tabs.map(tab=>"<li>"+tab.id+": "+tab.state+", v"+tab.version+"</li>").join("");document.querySelector("#events").textContent=JSON.stringify(result,null,2);document.querySelector("#receipt").value=result.pass?"PASS: replacement owner reconstructs and drains durable queue once":"FAIL"};document.querySelector("#run").addEventListener("click",runFixture);runFixture();
</script>
Run open multi-tab-coordination-fixture.html. Expected receipt: PASS: replacement owner reconstructs and drains durable queue once.
Keep protocols versioned and bounded
Both channels need an application protocol. Use discriminated message types, schema validation, maximum payload size, request identifiers, error envelopes, and a version negotiation or reload strategy. Never send secrets merely because the receiver shares an origin; XSS or compromised same-origin code remains in scope.
For worker ports, add request cancellation and timeouts so a dead owner does not leave unresolved UI promises. For broadcast, avoid request-response patterns that assume one peer will answer; if used as an optimization, give the requester a storage or server fallback. BroadcastChannel vs SharedWorker becomes maintainable when every message has a documented owner and failure path.
The local simulation uses notification, snapshot_request, snapshot_response, and checkpoint events. It caps its queue and records dropped delivery explicitly. A real implementation should also trace protocol version mismatches without logging draft contents or personal data.
Use capability detection and a server or per-tab fallback for unsupported environments. The fallback may duplicate live connections while preserving the same durable-state contract.
- Persist the feature's authoritative state in an appropriate store.
- Let an owner coordinate work while available.
- Use messages to announce changes, then re-read truth after uncertainty.
| Signal | Interpretation |
|---|---|
| Multi-tab reconstruction ledger | Three stacked ledgers distinguish ephemeral messages, live ownership, and durable records. |
Reconstruct before announcing readiness
At startup, open the durable store, validate its schema, recover pending effects, and compute the current version. Only then announce the tab or worker as ready. Buffer a small bounded set of incoming hints during initialization or simply re-read after the ready boundary. Do not let a fast broadcast overwrite state loaded from a slower but authoritative source.
The Yjs local-first editor may provide mergeable document state, yet presence and awareness remain ephemeral. BroadcastChannel vs SharedWorker still applies: notify peers about new updates or centralize a connection, while the document update log owns recovery. Name which layer is authoritative for each field.
The reconstruction ledger places durable truth at the bottom, live ownership above it, and hints at the top. That order is a useful code-review test: deletion of all ephemeral layers should degrade coordination, not erase accepted user work.
Reconstruction errors deserve a blocked state with recovery action. Announcing readiness from an empty default can overwrite a valid record produced by another context.
Choose from a failure matrix
Use BroadcastChannel when peers are symmetric, messages are advisory, startup can re-read truth, and no shared live resource needs one owner. Use SharedWorker when multiple tabs need a common execution context and the supported-browser matrix accepts it. Use a server, service worker, Web Lock, or storage transaction when those contracts better match lifetime and authority.
Test initial tab, two active tabs, late join, duplicate message, dropped message, frozen sender, closed receiver, worker termination, protocol upgrade, offline transition, and state reconstruction. The service-worker update flow is relevant when code-version mismatch spans tabs. BroadcastChannel vs SharedWorker passes when every case converges without treating message delivery as durable evidence.
Publish one decision table that labels notification, ownership, and persistence for the feature. That prevents a convenient API from quietly inheriting responsibilities its specification never promised.
Review privacy and sign-out behavior across every open tab. A logout hint should trigger authoritative session revalidation rather than act as the sole credential revocation mechanism. Also test a tab returning from suspension after account switching; it must discard stale cached identity before reading or announcing feature state to any newly connected peer. This multi-tab coordination check belongs in every release matrix.