[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-sharedworker-shared-tab-connection":44,"search-suggestions":60,"quiz-article-sharedworker-shared-tab-connection":109,"path-membership-sharedworker-shared-tab-connection":288},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a125ba-14fa-76aa-bbf5-759de7cadb69","sharedworker-shared-tab-connection","PRACTICE_QUIZ","SharedWorker — one connection, many tabs","Seven questions on how SharedWorker actually works: the connect event, port.start(), its lifecycle across tabs, the disconnect gotcha, and where it does (and doesn't) replace a leader-election pattern.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,101,105],{"slug":62,"name":63,"articles":64},"webdev","Webdev",140,{"slug":66,"name":67,"articles":68},"javascript","Javascript",114,{"slug":70,"name":71,"articles":72},"frontend","Frontend",85,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",53,{"slug":78,"name":79,"articles":80},"css","Css",42,{"slug":82,"name":83,"articles":84},"performance","Performance",22,{"slug":86,"name":87,"articles":88},"typescript","Typescript",18,{"slug":90,"name":91,"articles":92},"react","React",17,{"slug":94,"name":95,"articles":96},"browser","Browser",13,{"slug":98,"name":99,"articles":100},"node","Node",12,{"slug":102,"name":103,"articles":104},"programming","Programming",10,{"slug":106,"name":107,"articles":108},"accessibility","Accessibility",9,{"id":110,"slug":46,"title":111,"subtitle":52,"excerpt":112,"coverUrl":113,"locale":13,"readingMinutes":114,"publishedAt":115,"viewCount":116,"likeCount":19,"commentCount":19,"author":117,"vertical":122,"topic":123,"tags":125,"_count":130,"playground":132,"body":134,"bodyMd":274,"seo":275,"translationGroupId":278,"series":52,"podcastUrl":52,"verticalId":5,"thread":279,"assessments":281,"translations":284,"quiz":286},"01a125ba-14ad-725d-8e44-d97866cf2fee","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.","\u002Fmedia\u002Fcovers\u002Fsharedworker-shared-tab-connection.png",6,"2026-10-15T06:10:32.097Z",41,{"id":118,"name":119,"username":120,"avatarUrl":52,"headline":121},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":66,"name":124},"JavaScript",[126,127,128,129],{"slug":66,"name":67,"color":52},{"slug":62,"name":63,"color":52},{"slug":70,"name":71,"color":52},{"slug":82,"name":83,"color":52},{"assessments":131},1,{"slug":46,"title":133},"SharedWorker — interactive playground",{"blocks":135,"version":131},[136,140,143,146,150,153,159,162,165,168,172,176,179,182,185,188,191,194,197,200,203,206,210,213,216,219,222,225,228,231,234,237,240,243,246,249,256,259,262,265,268],{"id":137,"html":138,"type":139},"b1","\u003Cp>Last week&#39;s fix for &quot;six tabs, six WebSockets&quot; was \u003Ccode>navigator.locks\u003C\u002Fcode>: elect one tab as leader, let it hold the one real connection, relay everything to the rest over \u003Ccode>BroadcastChannel\u003C\u002Fcode>. It works. I said as much at the time.\u003C\u002Fp>","paragraph",{"id":141,"html":142,"type":139},"b2","\u003Cp>It also has a seam I didn&#39;t dwell on: \u003Cem>the connection lives inside a tab.\u003C\u002Fem> Close that tab — the user&#39;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 \u003Ccode>WebSocket\u003C\u002Fcode> 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.\u003C\u002Fp>",{"id":144,"html":145,"type":139},"b3","\u003Cp>There&#39;s a version of this where no tab ever owns the connection, so no tab closing can take it away.\u003C\u002Fp>",{"id":147,"html":148,"text":148,"type":149,"level":31},"b4","The belief: something has to own the socket","heading",{"id":151,"html":152,"type":139},"b5","\u003Cp>The leader-election pattern isn&#39;t wrong. It&#39;s the correct answer to a real constraint: a \u003Ccode>WebSocket\u003C\u002Fcode> object lives in \u003Cem>some\u003C\u002Fem> JavaScript execution context, and until recently the only context every tab could reliably reach was another tab&#39;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.\u003C\u002Fp>",{"id":154,"code":155,"type":156,"language":157,"highlight":158},"b6","\u002F\u002F what every tab currently runs\nnavigator.locks.request(\"app-connection-leader\", () => {\n  const socket = new WebSocket(\"wss:\u002F\u002Fexample.com\u002Flive\");\n  socket.onmessage = (e) => channel.postMessage(e.data);\n  return new Promise(() => {}); \u002F\u002F held until this tab closes\n});","code","js",[],{"id":160,"html":161,"type":139},"b7","\u003Cp>This is fine code. It&#39;s also solving &quot;how do we share one connection&quot; by picking which \u003Cem>page\u003C\u002Fem> gets to be the exception — which means the exception goes away the moment that page does.\u003C\u002Fp>",{"id":163,"html":164,"text":164,"type":149,"level":31},"b8","The context that outlives every tab",{"id":166,"html":167,"type":139},"b9","\u003Cp>A \u003Ccode>SharedWorker\u003C\u002Fcode> is a separate execution context, like a regular \u003Ccode>Worker\u003C\u002Fcode>, except more than one tab can connect to the \u003Cem>same instance\u003C\u002Fem> 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 \u003Ccode>MessagePort\u003C\u002Fcode> — a direct, two-way pipe into one shared scope that was never inside any of their tabs to begin with.\u003C\u002Fp>",{"id":169,"code":170,"type":156,"language":157,"highlight":171},"b10","\u002F\u002F live-feed-worker.js\nlet socket;\nconst ports = new Set();\n\nself.onconnect = (event) => {\n  const port = event.ports[0];\n  ports.add(port);\n\n  if (!socket) {\n    socket = new WebSocket(\"wss:\u002F\u002Fexample.com\u002Flive\");\n    socket.onmessage = (e) => {\n      for (const p of ports) p.postMessage(e.data);\n    };\n  }\n\n  port.onmessage = (e) => socket.send(e.data); \u002F\u002F tab -> server, through the one socket\n};",[],{"id":173,"code":174,"type":156,"language":157,"highlight":175},"b11","\u002F\u002F every tab, on load\nconst worker = new SharedWorker(\"\u002Flive-feed-worker.js\");\nworker.port.onmessage = (e) => renderUpdate(e.data);",[],{"id":177,"html":178,"type":139},"b12","\u003Cp>That&#39;s the whole wiring. No election, because there&#39;s nothing to elect a leader \u003Cem>for\u003C\u002Fem> — the worker opens the socket once, the first time \u003Ccode>ports.size\u003C\u002Fcode> goes from zero to one, and every tab after that just gets handed a port into a worker that&#39;s already running. Close the tab that happened to open first, second, or fifth: \u003Ccode>socket\u003C\u002Fcode> doesn&#39;t move, because it was never that tab&#39;s to carry.\u003C\u002Fp>",{"id":180,"html":181,"type":139},"b13","\u003Cp>One detail worth getting right: setting \u003Ccode>port.onmessage\u003C\u002Fcode> directly starts the port automatically. You only need an explicit \u003Ccode>port.start()\u003C\u002Fcode> call if you wire the port with \u003Ccode>addEventListener(&quot;message&quot;, …)\u003C\u002Fcode> instead — the implicit start only fires off the property setter.\u003C\u002Fp>",{"id":183,"html":184,"type":139},"b14","\u003C!-- playground:start -->",{"id":186,"html":187,"text":187,"type":149,"level":31},"b15","🎮 Try it yourself",{"id":189,"html":190,"type":139},"b16","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fsharedworker-shared-tab-connection\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":192,"html":193,"type":139},"b17","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":195,"html":196,"type":139},"b18","\u003C!-- playground:end -->",{"id":198,"html":199,"text":199,"type":149,"level":31},"b19","The puzzle this trades in",{"id":201,"html":202,"type":139},"b20","\u003Cp>Removing the leader doesn&#39;t remove every problem — it swaps one for a quieter one. \u003Ccode>navigator.locks\u003C\u002Fcode> 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 \u003Ccode>SharedWorker\u003C\u002Fcode> gets no such notice. There is no standard &quot;this port just disconnected&quot; event — a port left behind by a closed tab just sits in your \u003Ccode>Set\u003C\u002Fcode>, reachable only by \u003Ccode>postMessage\u003C\u002Fcode> calls that go nowhere, until the browser eventually garbage-collects it on its own schedule.\u003C\u002Fp>",{"id":204,"html":205,"type":139},"b21","\u003Cp>That&#39;s invisible at first — a few dead ports costs you nothing but a wasted loop iteration — but it means you can&#39;t use &quot;how many ports do I have&quot; as a live headcount, and you can&#39;t reliably ask &quot;is anyone still connected&quot; to decide when to close the real socket. The fix is the same one people have used since shared workers existed: don&#39;t wait to be told, say so on the way out.\u003C\u002Fp>",{"id":207,"code":208,"type":156,"language":157,"highlight":209},"b22","\u002F\u002F every tab\naddEventListener(\"pagehide\", () => {\n  worker.port.postMessage({ type: \"closing\" });\n});\n\n\u002F\u002F live-feed-worker.js\nport.onmessage = (e) => {\n  if (e.data?.type === \"closing\") {\n    ports.delete(port);\n    if (ports.size === 0) socket.close();\n    return;\n  }\n  socket.send(e.data);\n};",[],{"id":211,"html":212,"type":139},"b23","\u003Cp>Two extra lines on each side, and now the worker&#39;s idea of &quot;who&#39;s connected&quot; 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&#39;s already gone.\u003C\u002Fp>",{"id":214,"html":215,"text":215,"type":149,"level":31},"b24","Browser support, honestly",{"id":217,"html":218,"type":139},"b25","\u003Cp>Desktop Chrome, Firefox, and Edge have supported \u003Ccode>SharedWorker\u003C\u002Fcode> 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&#39;t support \u003Ccode>SharedWorker\u003C\u002Fcode> 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 \u003Ca href=\"https:\u002F\u002Fcaniuse.com\u002Fmdn-api_sharedworker\">caniuse.com&#39;s current numbers\u003C\u002Fa> against your own analytics before you rely on this being universal — and keep last week&#39;s \u003Ccode>navigator.locks\u003C\u002Fcode> pattern in your back pocket as the fallback that works everywhere today, Android included.\u003C\u002Fp>",{"id":220,"html":221,"text":221,"type":149,"level":31},"b26","No leader, no handoff, one quieter problem instead",{"id":223,"html":224,"type":139},"b27","\u003Cp>The leader-election pattern isn&#39;t obsolete — it&#39;s still the one answer that works on every browser shipping today, and it&#39;s still the right call if your Android numbers say you can&#39;t assume \u003Ccode>SharedWorker\u003C\u002Fcode> yet. But where it&#39;s available, it removes the actual thing that made leader election necessary: a connection that has to live inside some tab&#39;s lifetime. A \u003Ccode>SharedWorker\u003C\u002Fcode>&#39;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.\u003C\u002Fp>",{"id":226,"html":227,"type":139},"b28","\u003Cp>Go check whether your app&#39;s &quot;shared&quot; state actually lives independently of every tab, or just inside whichever one got there first. What&#39;s the oldest &quot;pick a tab to be special&quot; hack you&#39;re still running?\u003C\u002Fp>",{"id":229,"html":230,"type":139},"b29","\u003C!-- quiz:start -->",{"id":232,"html":233,"text":233,"type":149,"level":31},"b30","🧠 Test yourself",{"id":235,"html":236,"type":139},"b31","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fsharedworker-shared-tab-connection\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":238,"html":239,"type":139},"b32","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":241,"html":242,"type":139},"b33","\u003C!-- quiz:end -->",{"id":244,"html":245,"type":139},"b34","\u003C!-- related:start -->",{"id":247,"html":248,"text":248,"type":149,"level":31},"b35","📚 Read next",{"id":250,"type":251,"items":252,"ordered":18},"b36","list",[253,254,255],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpage-visibility-api-tab-aware-timers\">Your Timer Doesn&#39;t Know You Tabbed Away\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fbroadcast-channel-cross-tab-messaging\">Stop using the localStorage hack to sync browser tabs. BroadcastChannel does it natively.\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-composables-shared-state-trap\">Vue Composables: The Shared State Trap (+ Cheat Sheet)\u003C\u002Fa>",{"id":257,"html":258,"type":139},"b37","\u003C!-- related:end -->",{"id":260,"type":261},"b38","divider",{"id":263,"html":264,"type":139},"b39","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":266,"html":267,"type":139},"b40","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":269,"type":251,"items":270,"ordered":18},"b41",[271,272,273],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","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.\n\nIt 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.\n\nThere's a version of this where no tab ever owns the connection, so no tab closing can take it away.\n\n## The belief: something has to own the socket\n\nThe 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.\n\n```js\n\u002F\u002F what every tab currently runs\nnavigator.locks.request(\"app-connection-leader\", () => {\n  const socket = new WebSocket(\"wss:\u002F\u002Fexample.com\u002Flive\");\n  socket.onmessage = (e) => channel.postMessage(e.data);\n  return new Promise(() => {}); \u002F\u002F held until this tab closes\n});\n```\n\nThis 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.\n\n## The context that outlives every tab\n\nA `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.\n\n```js\n\u002F\u002F live-feed-worker.js\nlet socket;\nconst ports = new Set();\n\nself.onconnect = (event) => {\n  const port = event.ports[0];\n  ports.add(port);\n\n  if (!socket) {\n    socket = new WebSocket(\"wss:\u002F\u002Fexample.com\u002Flive\");\n    socket.onmessage = (e) => {\n      for (const p of ports) p.postMessage(e.data);\n    };\n  }\n\n  port.onmessage = (e) => socket.send(e.data); \u002F\u002F tab -> server, through the one socket\n};\n```\n\n```js\n\u002F\u002F every tab, on load\nconst worker = new SharedWorker(\"\u002Flive-feed-worker.js\");\nworker.port.onmessage = (e) => renderUpdate(e.data);\n```\n\nThat'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.\n\nOne 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.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fsharedworker-shared-tab-connection\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The puzzle this trades in\n\nRemoving 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.\n\nThat'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.\n\n```js\n\u002F\u002F every tab\naddEventListener(\"pagehide\", () => {\n  worker.port.postMessage({ type: \"closing\" });\n});\n\n\u002F\u002F live-feed-worker.js\nport.onmessage = (e) => {\n  if (e.data?.type === \"closing\") {\n    ports.delete(port);\n    if (ports.size === 0) socket.close();\n    return;\n  }\n  socket.send(e.data);\n};\n```\n\nTwo 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.\n\n## Browser support, honestly\n\nDesktop 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](https:\u002F\u002Fcaniuse.com\u002Fmdn-api_sharedworker) 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.\n\n## No leader, no handoff, one quieter problem instead\n\nThe 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.\n\nGo 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?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fsharedworker-shared-tab-connection\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [Your Timer Doesn't Know You Tabbed Away](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpage-visibility-api-tab-aware-timers)\n- [Stop using the localStorage hack to sync browser tabs. BroadcastChannel does it natively.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fbroadcast-channel-cross-tab-messaging)\n- [Vue Composables: The Shared State Trap (+ Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-composables-shared-state-trap)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":111,"canonical":276,"description":277},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fsharedworker-shared-tab-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","01a125ba-14ad-725d-8e44-de1c5cbf9234",{"id":280,"locked":18},"01a125ba-14d9-7649-a998-0b3d1605e5f1",[282],{"id":45,"slug":46,"title":48,"_count":283},{"questions":51},[285],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":287,"questionCount":51},{"questions":51},[]]