Stop Electing a Leader Tab. SharedWorker Just Holds the Connection.
Electing a leader tab to own one shared WebSocket works great — until the leader is the tab someone closes. SharedWorker skips the election: the connection lives in the worker, not in any tab, so there's no leader to lose.

Last week's fix for "six tabs, six WebSockets" was navigator.locks: elect one tab as leader, let it hold the one real connection, relay everything to the rest over BroadcastChannel. It works. I said as much at the time.
It also has a seam I didn't dwell on: the connection lives inside a tab. Close that tab — the user's doing, not a bug — and the lock releases, a new leader gets picked within the same event loop turn, and that new leader still has to open a brand-new WebSocket from scratch. Real handshake, real round trip, real gap before updates resume. For a dashboard, nobody notices. For a live price ticker or a shared cursor, that gap is the whole ballgame.
There's a version of this where no tab ever owns the connection, so no tab closing can take it away.
The leader-election pattern isn't wrong. It's the correct answer to a real constraint: a WebSocket object lives in some JavaScript execution context, and until recently the only context every tab could reliably reach was another tab's. So you pick one, crown it leader, and accept that leadership — like any job held by one person — has a handoff cost when that person leaves.
This is fine code. It's also solving "how do we share one connection" by picking which page gets to be the exception — which means the exception goes away the moment that page does.
A SharedWorker is a separate execution context, like a regular Worker, except more than one tab can connect to the same instance of it at once. The browser starts it the first time any tab asks for it by URL, keeps it running as long as at least one tab is still connected, and every tab talks to it through its own MessagePort — a direct, two-way pipe into one shared scope that was never inside any of their tabs to begin with.
That's the whole wiring. No election, because there's nothing to elect a leader for — the worker opens the socket once, the first time ports.size goes from zero to one, and every tab after that just gets handed a port into a worker that's already running. Close the tab that happened to open first, second, or fifth: socket doesn't move, because it was never that tab's to carry.
One detail worth getting right: setting port.onmessage directly starts the port automatically. You only need an explicit port.start() call if you wire the port with addEventListener("message", …) instead — the implicit start only fires off the property setter.
Runs right in your browser — poke at it and watch the concept react live.
Removing the leader doesn't remove every problem — it swaps one for a quieter one. navigator.locks tells you, immediately and for free, the moment a tab is gone: the lock releases the instant the tab closes, crashes, or navigates away, because the browser is the one holding the lock and the browser knows. A SharedWorker gets no such notice. There is no standard "this port just disconnected" event — a port left behind by a closed tab just sits in your Set, reachable only by postMessage calls that go nowhere, until the browser eventually garbage-collects it on its own schedule.
That's invisible at first — a few dead ports costs you nothing but a wasted loop iteration — but it means you can't use "how many ports do I have" as a live headcount, and you can't reliably ask "is anyone still connected" to decide when to close the real socket. The fix is the same one people have used since shared workers existed: don't wait to be told, say so on the way out.
Two extra lines on each side, and now the worker's idea of "who's connected" matches reality closely enough to act on — close the real socket once the last tab says goodbye, instead of leaving it open for a page that's already gone.
Desktop Chrome, Firefox, and Edge have supported SharedWorker for a very long time, and Safari added it in version 16 — including iOS Safari 16, which is the gap that used to rule this API out for anything mobile-facing. The one real asterisk: Chrome for Android didn't support SharedWorker at all for years, specifically because of unpredictable process-lifecycle behavior on mobile, and only gained support in a 2026 release. If a meaningful slice of your traffic is Android Chrome on an older version, check caniuse.com's current numbers against your own analytics before you rely on this being universal — and keep last week's navigator.locks pattern in your back pocket as the fallback that works everywhere today, Android included.
The leader-election pattern isn't obsolete — it's still the one answer that works on every browser shipping today, and it's still the right call if your Android numbers say you can't assume SharedWorker yet. But where it's available, it removes the actual thing that made leader election necessary: a connection that has to live inside some tab's lifetime. A SharedWorker's connection lives in its own context, outside every tab equally, so closing any one of them — including the tab that happened to open it first — costs nothing at all.
Go check whether your app's "shared" state actually lives independently of every tab, or just inside whichever one got there first. What's the oldest "pick a tab to be special" hack you're still running?
Think it clicked? Take the 7-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
- Your Timer Doesn't Know You Tabbed Away
- Stop using the localStorage hack to sync browser tabs. BroadcastChannel does it natively.
- Vue Composables: The Shared State Trap (+ Cheat Sheet)
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
Keep reading
One post a day, in your inbox
Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.
0 comments
Sign in to join the discussion, like comments, and save articles for later.