[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"search-suggestions":32,"article-web-locks-api-tab-coordination":79,"related-web-locks-api-tab-coordination":257,"code:js:true:1acnz1q":386,"code:js:true:f40zy2":387},[4,20],{"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,[33,37,41,45,49,53,57,61,65,69,73,76],{"slug":34,"name":35,"articles":36},"webdev","Webdev",54,{"slug":38,"name":39,"articles":40},"javascript","Javascript",49,{"slug":42,"name":43,"articles":44},"frontend","Frontend",48,{"slug":46,"name":47,"articles":48},"css","Css",19,{"slug":50,"name":51,"articles":52},"typescript","Typescript",10,{"slug":54,"name":55,"articles":56},"performance","Performance",8,{"slug":58,"name":59,"articles":60},"grammar","Grammar",6,{"slug":62,"name":63,"articles":64},"react","React",5,{"slug":66,"name":67,"articles":68},"node","Node",4,{"slug":70,"name":71,"articles":72},"ai","Ai",3,{"slug":74,"name":75,"articles":72},"html","Html",{"slug":77,"name":78,"articles":72},"tutorial","Tutorial",{"id":80,"slug":81,"title":82,"subtitle":83,"excerpt":84,"coverUrl":85,"locale":13,"readingMinutes":64,"publishedAt":86,"viewCount":87,"likeCount":19,"commentCount":19,"author":88,"vertical":93,"topic":94,"tags":96,"_count":103,"playground":105,"body":107,"bodyMd":241,"seo":242,"translationGroupId":245,"thread":246,"assessments":248,"translations":253,"quiz":255},"019ff19b-f9ca-772e-8f09-cd3d3c640d2b","web-locks-api-tab-coordination","Five tabs open, one refresh token — the race nobody noticed",null,"Users with multiple tabs open triggered a silent race: every tab refreshed the same expiring auth token at once, and the server started invalidating its own sessions. navigator.locks fixes it with a real mutex, no polling or localStorage hacks required.","\u002Fmedia\u002Fcovers\u002Fweb-locks-api-tab-coordination.png","2026-08-15T06:44:54.917Z",88,{"id":89,"name":90,"username":91,"avatarUrl":83,"headline":92},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":38,"name":95},"JavaScript",[97,98,99,100],{"slug":38,"name":39,"color":83},{"slug":34,"name":35,"color":83},{"slug":42,"name":43,"color":83},{"slug":101,"name":102,"color":83},"async","Async",{"assessments":104},1,{"slug":81,"title":106},"navigator.locks — the four-tabs race, live",{"blocks":108,"version":104},[109,113,116,120,123,126,130,133,139,142,145,148,151,154,157,161,164,168,171,174,177,180,187,190,194,197,200,203,206,209,212,215,218,221,224,227,230,233],{"id":110,"html":111,"type":112},"b1","\u003Cp>A user reports that they keep getting logged out. Not immediately — after a while, randomly, always mid-task. You can&#39;t reproduce it. You check the server logs and find something stranger than a bug report: the same refresh token, submitted four times in the same eleven-millisecond window, from the same user, same IP, same session. The server does what any reasonable auth server does with a reused refresh token — it assumes theft and revokes the whole session.\u003C\u002Fp>","paragraph",{"id":114,"html":115,"type":112},"b2","\u003Cp>The user didn&#39;t do anything wrong. They just had four tabs open.\u003C\u002Fp>",{"id":117,"html":118,"text":118,"type":119,"level":31},"b3","Why one user becomes four requests","heading",{"id":121,"html":122,"type":112},"b4","\u003Cp>Your access token expires. Every tab that&#39;s currently open holds its own copy of the JavaScript running your app, and every one of those copies is watching the same clock. The moment the token goes stale, each tab&#39;s fetch wrapper notices independently and does the sensible thing: call the refresh endpoint before retrying the failed request.\u003C\u002Fp>",{"id":124,"html":125,"type":112},"b5","\u003Cp>Four tabs, four independent &quot;sensible things,&quot; at nearly the same instant. The server sees four refresh attempts for one token. Depending on how strict your rotation policy is, the second one in either succeeds and burns the token for the other three, or the server flags it as replay and kills the session outright. Either way, the user gets logged out for the crime of having your app open twice.\u003C\u002Fp>",{"id":127,"html":128,"text":129,"type":119,"level":31},"b6","The fix that looks right and isn&#39;t","The fix that looks right and isn't",{"id":131,"html":132,"type":112},"b7","\u003Cp>The instinct is to reach for \u003Ccode>localStorage\u003C\u002Fcode> as a shared flag, since it&#39;s the one thing every tab on the origin can already see:\u003C\u002Fp>",{"id":134,"code":135,"type":136,"language":137,"highlight":138},"b8","\u002F\u002F Looks like a mutex. Isn't one.\nasync function refreshIfNeeded() {\n  if (localStorage.getItem('refreshing') === 'true') {\n    await waitForFlagToClear();\n    return;\n  }\n  localStorage.setItem('refreshing', 'true');\n  await refreshToken();\n  localStorage.removeItem('refreshing');\n}","code","js",[],{"id":140,"html":141,"type":112},"b9","\u003Cp>This passes every manual test you run by clicking around in two tabs, because you&#39;re slower than the bug. The problem is that &quot;check the flag, then set the flag&quot; is two separate operations, and nothing stops two tabs from both running the check \u003Cem>before\u003C\u002Fem> either of them runs the set. \u003Ccode>localStorage\u003C\u002Fcode> reads and writes aren&#39;t atomic across tabs — there&#39;s no lock on the lock. You&#39;ve built the exact race you were trying to prevent, just with extra steps and a \u003Ccode>waitForFlagToClear\u003C\u002Fcode> polling loop bolted on.\u003C\u002Fp>",{"id":143,"html":144,"type":112},"b10","\u003C!-- playground:start -->",{"id":146,"html":147,"text":147,"type":119,"level":31},"b11","🎮 Try it yourself",{"id":149,"html":150,"type":112},"b12","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweb-locks-api-tab-coordination\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":152,"html":153,"type":112},"b13","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":155,"html":156,"type":112},"b14","\u003C!-- playground:end -->",{"id":158,"html":159,"text":160,"type":119,"level":31},"b15","The actual fix: \u003Ccode>navigator.locks\u003C\u002Fcode>","The actual fix: navigator.locks",{"id":162,"html":163,"type":112},"b16","\u003Cp>The Web Locks API gives you what \u003Ccode>localStorage\u003C\u002Fcode> was pretending to be: a real, origin-scoped mutex that the browser itself arbitrates, shared across every tab, iframe, and worker on that origin.\u003C\u002Fp>",{"id":165,"code":166,"type":136,"language":137,"highlight":167},"b17","async function refreshIfNeeded() {\n  return navigator.locks.request('refresh-token', async (lock) => {\n    \u002F\u002F Only one tab's callback runs at a time for this lock name.\n    \u002F\u002F Everyone else queues here until it's their turn.\n    if (tokenIsStillValid()) return; \u002F\u002F by the time we got the lock, someone else already refreshed\n    await refreshToken();\n  });\n}",[],{"id":169,"html":170,"type":112},"b18","\u003Cp>\u003Ccode>navigator.locks.request(name, callback)\u003C\u002Fcode> returns a promise that resolves once your callback&#39;s own returned promise settles — and the lock is held for exactly that long. Call it from all four tabs at once and the browser queues three of them. The first one through does the actual refresh; by the time the second tab gets its turn, the token check at the top short-circuits and it does nothing. No polling, no flag to forget to clear, no window where two tabs both think they&#39;re first.\u003C\u002Fp>",{"id":172,"html":173,"type":112},"b19","\u003Cp>The \u003Ccode>mode\u003C\u002Fcode> option is where it earns its name: \u003Ccode>exclusive\u003C\u002Fcode> (the default) allows one holder, \u003Ccode>shared\u003C\u002Fcode> allows many readers at once as long as no writer is waiting — the same read\u002Fwrite distinction you&#39;d reach for with any mutex. Pass \u003Ccode>{ ifAvailable: true }\u003C\u002Fcode> if you want to try the lock without waiting, or a \u003Ccode>signal\u003C\u002Fcode> if the wait itself needs to be cancellable.\u003C\u002Fp>",{"id":175,"html":176,"text":176,"type":119,"level":31},"b20","Where else this shows up",{"id":178,"html":179,"type":112},"b21","\u003Cp>Token refresh is the clearest case, but the pattern is &quot;N tabs, one shared resource, exactly-once semantics&quot; — and that shows up more than you&#39;d think:\u003C\u002Fp>",{"id":181,"type":182,"items":183,"ordered":18},"b22","list",[184,185,186],"\u003Cstrong>IndexedDB migrations.\u003C\u002Fstrong> A schema upgrade that runs once per version, not once per open tab.","\u003Cstrong>Single-flight cache fills.\u003C\u002Fstrong> Five tabs all miss a cache entry at once; you want one network request, not five.","\u003Cstrong>Cross-tab leader election.\u003C\u002Fstrong> One tab should own a websocket or a polling interval; the rest should just listen. Hold a lock for as long as the tab is &quot;leader,&quot; and release it (close the tab, or explicitly) to let another tab take over.",{"id":188,"html":189,"type":112},"b23","\u003Cp>Each of these is a version of the same bug: code that&#39;s correct for one execution context quietly breaks the moment a user does something completely reasonable, like opening a second tab.\u003C\u002Fp>",{"id":191,"html":192,"text":193,"type":119,"level":31},"b24","What this isn&#39;t","What this isn't",{"id":195,"html":196,"type":112},"b25","\u003Cp>\u003Ccode>navigator.locks\u003C\u002Fcode> doesn&#39;t send data between tabs — that&#39;s \u003Ccode>BroadcastChannel\u003C\u002Fcode>&#39;s job, and the two pair well together (grab the lock, do the work, broadcast the result to whoever&#39;s waiting). Locks answer &quot;who goes first,&quot; not &quot;who else needs to know.&quot; Confusing the two gets you either a mutex trying to carry a payload, or a message channel trying to enforce mutual exclusion — both are the wrong tool wearing the other one&#39;s hat.\u003C\u002Fp>",{"id":198,"html":199,"text":199,"type":119,"level":31},"b26","Browser support",{"id":201,"html":202,"type":112},"b27","\u003Cp>\u003Ccode>navigator.locks\u003C\u002Fcode> shipped in Chrome 69 (2018), Firefox 96 (2022), and Safari 15.4 (2022) — all three major engines have had it for years, so it&#39;s safe to reach for directly rather than feature-detecting a fallback.\u003C\u002Fp>",{"id":204,"html":205,"type":112},"b28","\u003C!-- quiz:start -->",{"id":207,"html":208,"text":208,"type":119,"level":31},"b29","🧠 Test yourself",{"id":210,"html":211,"type":112},"b30","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweb-locks-api-tab-coordination\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":213,"html":214,"type":112},"b31","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":216,"html":217,"type":112},"b32","\u003C!-- quiz:end -->",{"id":219,"html":220,"text":220,"type":119,"level":31},"b33","The takeaway",{"id":222,"html":223,"type":112},"b34","\u003Cp>If your app has ever logged a user out for no reason they could explain, check whether the fix you&#39;d reach for — a flag in \u003Ccode>localStorage\u003C\u002Fcode>, a &quot;just add a delay&quot; hack — is really a mutex, or just something that looks like one until two tabs hit it at the same millisecond. \u003Ccode>navigator.locks.request()\u003C\u002Fcode> is the browser handing you a real one, for free, scoped exactly where you need it: across every tab your user happens to have open.\u003C\u002Fp>",{"id":225,"html":226,"type":112},"b35","\u003Cp>Have you shipped a bug that only showed up with multiple tabs open? What was it, and how long did it take to reproduce?\u003C\u002Fp>",{"id":228,"type":229},"b36","divider",{"id":231,"html":232,"type":112},"b37","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":234,"type":182,"items":235,"ordered":18},"b38",[236,237,238,239,240],"⭐ \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>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>","A user reports that they keep getting logged out. Not immediately — after a while, randomly, always mid-task. You can't reproduce it. You check the server logs and find something stranger than a bug report: the same refresh token, submitted four times in the same eleven-millisecond window, from the same user, same IP, same session. The server does what any reasonable auth server does with a reused refresh token — it assumes theft and revokes the whole session.\n\nThe user didn't do anything wrong. They just had four tabs open.\n\n## Why one user becomes four requests\n\nYour access token expires. Every tab that's currently open holds its own copy of the JavaScript running your app, and every one of those copies is watching the same clock. The moment the token goes stale, each tab's fetch wrapper notices independently and does the sensible thing: call the refresh endpoint before retrying the failed request.\n\nFour tabs, four independent \"sensible things,\" at nearly the same instant. The server sees four refresh attempts for one token. Depending on how strict your rotation policy is, the second one in either succeeds and burns the token for the other three, or the server flags it as replay and kills the session outright. Either way, the user gets logged out for the crime of having your app open twice.\n\n## The fix that looks right and isn't\n\nThe instinct is to reach for `localStorage` as a shared flag, since it's the one thing every tab on the origin can already see:\n\n```js\n\u002F\u002F Looks like a mutex. Isn't one.\nasync function refreshIfNeeded() {\n  if (localStorage.getItem('refreshing') === 'true') {\n    await waitForFlagToClear();\n    return;\n  }\n  localStorage.setItem('refreshing', 'true');\n  await refreshToken();\n  localStorage.removeItem('refreshing');\n}\n```\n\nThis passes every manual test you run by clicking around in two tabs, because you're slower than the bug. The problem is that \"check the flag, then set the flag\" is two separate operations, and nothing stops two tabs from both running the check *before* either of them runs the set. `localStorage` reads and writes aren't atomic across tabs — there's no lock on the lock. You've built the exact race you were trying to prevent, just with extra steps and a `waitForFlagToClear` polling loop bolted on.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweb-locks-api-tab-coordination\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 actual fix: `navigator.locks`\n\nThe Web Locks API gives you what `localStorage` was pretending to be: a real, origin-scoped mutex that the browser itself arbitrates, shared across every tab, iframe, and worker on that origin.\n\n```js\nasync function refreshIfNeeded() {\n  return navigator.locks.request('refresh-token', async (lock) => {\n    \u002F\u002F Only one tab's callback runs at a time for this lock name.\n    \u002F\u002F Everyone else queues here until it's their turn.\n    if (tokenIsStillValid()) return; \u002F\u002F by the time we got the lock, someone else already refreshed\n    await refreshToken();\n  });\n}\n```\n\n`navigator.locks.request(name, callback)` returns a promise that resolves once your callback's own returned promise settles — and the lock is held for exactly that long. Call it from all four tabs at once and the browser queues three of them. The first one through does the actual refresh; by the time the second tab gets its turn, the token check at the top short-circuits and it does nothing. No polling, no flag to forget to clear, no window where two tabs both think they're first.\n\nThe `mode` option is where it earns its name: `exclusive` (the default) allows one holder, `shared` allows many readers at once as long as no writer is waiting — the same read\u002Fwrite distinction you'd reach for with any mutex. Pass `{ ifAvailable: true }` if you want to try the lock without waiting, or a `signal` if the wait itself needs to be cancellable.\n\n## Where else this shows up\n\nToken refresh is the clearest case, but the pattern is \"N tabs, one shared resource, exactly-once semantics\" — and that shows up more than you'd think:\n\n- **IndexedDB migrations.** A schema upgrade that runs once per version, not once per open tab.\n- **Single-flight cache fills.** Five tabs all miss a cache entry at once; you want one network request, not five.\n- **Cross-tab leader election.** One tab should own a websocket or a polling interval; the rest should just listen. Hold a lock for as long as the tab is \"leader,\" and release it (close the tab, or explicitly) to let another tab take over.\n\nEach of these is a version of the same bug: code that's correct for one execution context quietly breaks the moment a user does something completely reasonable, like opening a second tab.\n\n## What this isn't\n\n`navigator.locks` doesn't send data between tabs — that's `BroadcastChannel`'s job, and the two pair well together (grab the lock, do the work, broadcast the result to whoever's waiting). Locks answer \"who goes first,\" not \"who else needs to know.\" Confusing the two gets you either a mutex trying to carry a payload, or a message channel trying to enforce mutual exclusion — both are the wrong tool wearing the other one's hat.\n\n## Browser support\n\n`navigator.locks` shipped in Chrome 69 (2018), Firefox 96 (2022), and Safari 15.4 (2022) — all three major engines have had it for years, so it's safe to reach for directly rather than feature-detecting a fallback.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweb-locks-api-tab-coordination\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## The takeaway\n\nIf your app has ever logged a user out for no reason they could explain, check whether the fix you'd reach for — a flag in `localStorage`, a \"just add a delay\" hack — is really a mutex, or just something that looks like one until two tabs hit it at the same millisecond. `navigator.locks.request()` is the browser handing you a real one, for free, scoped exactly where you need it: across every tab your user happens to have open.\n\nHave you shipped a bug that only showed up with multiple tabs open? What was it, and how long did it take to reproduce?\n\n---\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)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":82,"canonical":243,"description":244},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweb-locks-api-tab-coordination","Users with multiple tabs open triggered a silent race: every tab refreshed the same expiring auth token at once, and the server started invalidating its own sessions. navigator.loc","019ff19b-f9ca-772e-8f09-d2c1943af8ab",{"id":247,"locked":18},"019ff19b-f9f2-7468-ad20-ad637d68ac38",[249],{"id":250,"slug":81,"title":251,"_count":252},"019ff19b-fa13-7063-9dee-9f55f4a01543","navigator.locks — the four-tabs race",{"questions":56},[254],{"locale":13,"slug":81},{"id":250,"slug":81,"title":251,"_count":256,"questionCount":56},{"questions":56},{"items":258,"meta":384},[259,271,289,307,326,346,364],{"id":80,"slug":81,"title":82,"subtitle":83,"excerpt":84,"coverUrl":85,"locale":13,"readingMinutes":64,"publishedAt":86,"viewCount":260,"likeCount":19,"commentCount":19,"author":261,"vertical":262,"topic":263,"tags":264,"_count":269,"playground":270,"hasQuiz":17,"hasPlayground":17},89,{"id":89,"name":90,"username":91,"avatarUrl":83,"headline":92},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":38,"name":95},[265,266,267,268],{"slug":38,"name":39,"color":83},{"slug":34,"name":35,"color":83},{"slug":42,"name":43,"color":83},{"slug":101,"name":102,"color":83},{"assessments":104},{"slug":81},{"id":272,"slug":273,"title":274,"subtitle":83,"excerpt":275,"coverUrl":276,"locale":13,"readingMinutes":72,"publishedAt":277,"viewCount":278,"likeCount":19,"commentCount":19,"author":279,"vertical":280,"topic":281,"tags":282,"_count":287,"playground":288,"hasQuiz":17,"hasPlayground":17},"019fe660-8bc3-7444-8c96-86953902f5e0","findlast-search-from-end","You copy and reverse the array to find the last match. `findLast()` searches from the end directly.","The usual workaround — `[...arr].reverse().find()` — allocates a full copy just to walk it backward. ES2023 added `findLast()` and `findLastIndex()` to search from the end without touching the original.","\u002Fmedia\u002Fcovers\u002Ffindlast-search-from-end.png","2026-08-14T07:38:34.486Z",151,{"id":89,"name":90,"username":91,"avatarUrl":83,"headline":92},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":38,"name":95},[283,284,285,286],{"slug":38,"name":39,"color":83},{"slug":34,"name":35,"color":83},{"slug":42,"name":43,"color":83},{"slug":50,"name":51,"color":83},{"assessments":104},{"slug":273},{"id":290,"slug":291,"title":292,"subtitle":83,"excerpt":293,"coverUrl":294,"locale":13,"readingMinutes":68,"publishedAt":295,"viewCount":296,"likeCount":19,"commentCount":19,"author":297,"vertical":298,"topic":299,"tags":300,"_count":305,"playground":306,"hasQuiz":17,"hasPlayground":17},"019fe661-0450-720a-ad6e-5eb995429cd7","sendbeacon-reliable-unload-data","Your `fetch()` in `beforeunload` is being silently dropped. Use `navigator.sendBeacon()`.","Browsers cancel in-flight network requests when a page unloads. The common fix — synchronous XHR — is deprecated. `navigator.sendBeacon()` is the correct, fire-and-forget API designed exactly for this case.","\u002Fmedia\u002Fcovers\u002Fsendbeacon-reliable-unload-data.png","2026-08-13T07:40:48.196Z",212,{"id":89,"name":90,"username":91,"avatarUrl":83,"headline":92},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":38,"name":95},[301,302,303,304],{"slug":38,"name":39,"color":83},{"slug":34,"name":35,"color":83},{"slug":42,"name":43,"color":83},{"slug":54,"name":55,"color":83},{"assessments":104},{"slug":291},{"id":308,"slug":309,"title":310,"subtitle":83,"excerpt":311,"coverUrl":312,"locale":13,"readingMinutes":68,"publishedAt":313,"viewCount":314,"likeCount":19,"commentCount":19,"author":315,"vertical":316,"topic":317,"tags":319,"_count":324,"playground":325,"hasQuiz":17,"hasPlayground":17},"019ff19b-f763-70e0-abdf-eded59c8e69a","overscroll-behavior-scroll-containment","You're blocking touchmove events to contain scroll. `overscroll-behavior` does it natively.","When a scrollable container hits its edge, the page behind it starts scrolling too. Developers block this with JavaScript event handlers or body overflow tricks. overscroll-behavior is the CSS property that turns off scroll chaining in one line.","\u002Fmedia\u002Fcovers\u002Foverscroll-behavior-scroll-containment.png","2026-08-12T07:38:53.786Z",234,{"id":89,"name":90,"username":91,"avatarUrl":83,"headline":92},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":46,"name":318},"CSS",[320,321,322,323],{"slug":46,"name":47,"color":83},{"slug":34,"name":35,"color":83},{"slug":42,"name":43,"color":83},{"slug":38,"name":39,"color":83},{"assessments":104},{"slug":309},{"id":327,"slug":328,"title":329,"subtitle":83,"excerpt":330,"coverUrl":331,"locale":13,"readingMinutes":68,"publishedAt":332,"viewCount":333,"likeCount":19,"commentCount":19,"author":334,"vertical":335,"topic":336,"tags":337,"_count":344,"playground":345,"hasQuiz":17,"hasPlayground":17},"019ff19b-f68d-77ea-9a2c-70e14c5c7a05","mutation-observer-dom-change-detection","You're polling setInterval to detect DOM changes. `MutationObserver` fires when they happen.","Watching for DOM mutations with setInterval or custom event dispatch is fragile and imprecise. The MutationObserver API delivers a callback exactly when attributes, text content, or child elements change — with full control over which types of mutations you watch.","\u002Fmedia\u002Fcovers\u002Fmutation-observer-dom-change-detection.png","2026-08-11T15:48:01.781Z",456,{"id":89,"name":90,"username":91,"avatarUrl":83,"headline":92},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":38,"name":95},[338,339,340,341],{"slug":38,"name":39,"color":83},{"slug":34,"name":35,"color":83},{"slug":42,"name":43,"color":83},{"slug":342,"name":343,"color":83},"dom","Dom",{"assessments":104},{"slug":328},{"id":347,"slug":348,"title":349,"subtitle":83,"excerpt":350,"coverUrl":351,"locale":13,"readingMinutes":68,"publishedAt":352,"viewCount":353,"likeCount":19,"commentCount":19,"author":354,"vertical":355,"topic":356,"tags":357,"_count":362,"playground":363,"hasQuiz":17,"hasPlayground":17},"019ff19b-f5b3-7569-8cc8-22b1136aa9b2","compressionstream-native-gzip","You're importing pako to gzip data. `CompressionStream` does it natively.","pako is one of npm's most downloaded packages — pulled in by thousands of projects to compress data before storing or sending it. CompressionStream and DecompressionStream do the same job natively, in any modern browser and Node.js 18+, with no dependencies and native C-speed execution.","\u002Fmedia\u002Fcovers\u002Fcompressionstream-native-gzip.png","2026-08-10T07:49:27.000Z",292,{"id":89,"name":90,"username":91,"avatarUrl":83,"headline":92},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":38,"name":95},[358,359,360,361],{"slug":38,"name":39,"color":83},{"slug":34,"name":35,"color":83},{"slug":42,"name":43,"color":83},{"slug":54,"name":55,"color":83},{"assessments":104},{"slug":348},{"id":365,"slug":366,"title":367,"subtitle":83,"excerpt":368,"coverUrl":369,"locale":13,"readingMinutes":68,"publishedAt":370,"viewCount":371,"likeCount":19,"commentCount":19,"author":372,"vertical":373,"topic":374,"tags":375,"_count":382,"playground":383,"hasQuiz":17,"hasPlayground":17},"019fe660-fcbe-745e-a443-5ae33ac10aaf","broadcast-channel-cross-tab-messaging","Stop using the localStorage hack to sync browser tabs. BroadcastChannel does it natively.","Syncing state across browser tabs with localStorage events is a widely-used trick that requires JSON.stringify, event filtering, and careful cleanup. The Broadcast Channel API delivers messages between tabs directly, with none of the side effects.","\u002Fmedia\u002Fcovers\u002Fbroadcast-channel-cross-tab-messaging.png","2026-08-09T07:02:41.629Z",361,{"id":89,"name":90,"username":91,"avatarUrl":83,"headline":92},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":38,"name":95},[376,377,378,379],{"slug":38,"name":39,"color":83},{"slug":34,"name":35,"color":83},{"slug":42,"name":43,"color":83},{"slug":380,"name":381,"color":83},"apis","Apis",{"assessments":104},{"slug":366},{"page":104,"perPage":385,"total":40,"totalPages":385},7,"\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F Looks like a mutex. Isn't one.\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">refreshIfNeeded\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>localStorage\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getItem\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'refreshing'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'true'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">waitForFlagToClear\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  localStorage\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">setItem\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'refreshing'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'true'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">refreshToken\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  localStorage\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">removeItem\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'refreshing'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">refreshIfNeeded\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> navigator\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>locks\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">request\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'refresh-token'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>lock\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-cmnt\">\u002F\u002F Only one tab's callback runs at a time for this lock name.\n\u003C\u002Fspan>    \u003Cspan class=\"shj-cmnt\">\u002F\u002F Everyone else queues here until it's their turn.\n\u003C\u002Fspan>    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-func\">tokenIsStillValid\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F by the time we got the lock, someone else already refreshed\n\u003C\u002Fspan>    \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">refreshToken\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>"]