[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-bfcache-beforeunload-back-button":44,"search-suggestions":60,"quiz-article-bfcache-beforeunload-back-button":107},[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},"01a0f0ef-da04-723d-a246-7a9949fac8d5","bfcache-beforeunload-back-button","PRACTICE_QUIZ","bfcache, beforeunload, and pagehide","Eight questions on how the back\u002Fforward cache actually works, what disqualifies a page from it, and the pagehide\u002Fpageshow replacement for unload-based cleanup.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,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,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",124,{"slug":66,"name":67,"articles":68},"javascript","Javascript",103,{"slug":70,"name":71,"articles":72},"frontend","Frontend",79,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",48,{"slug":78,"name":79,"articles":80},"css","Css",40,{"slug":82,"name":83,"articles":84},"typescript","Typescript",18,{"slug":86,"name":87,"articles":88},"performance","Performance",16,{"slug":90,"name":91,"articles":92},"react","React",15,{"slug":94,"name":95,"articles":96},"browser","Browser",11,{"slug":98,"name":99,"articles":96},"node","Node",{"slug":101,"name":102,"articles":103},"html","Html",9,{"slug":105,"name":106,"articles":51},"accessibility","Accessibility",{"id":108,"slug":46,"title":109,"subtitle":52,"excerpt":110,"coverUrl":111,"locale":13,"readingMinutes":112,"publishedAt":113,"viewCount":114,"likeCount":19,"commentCount":19,"author":115,"vertical":120,"topic":121,"tags":123,"_count":128,"playground":130,"body":132,"bodyMd":279,"seo":280,"translationGroupId":283,"series":52,"podcastUrl":52,"verticalId":5,"thread":284,"assessments":286,"translations":289,"quiz":291},"01a0f0ef-d9b8-719b-97a3-fb43e840c4d1","`beforeunload` Can Still Kill Your bfcache. Mount It Only When Dirty.","A `beforeunload` listener you left mounted forever can still quietly cost you bfcache eligibility — and an `unload` listener reliably will. Here's the pagehide\u002Fpageshow fix, and why 'mount it only when there's something to lose' is the durable rule.","\u002Fmedia\u002Fcovers\u002Fbfcache-beforeunload-back-button.png",6,"2026-10-05T06:10:30.618Z",46,{"id":116,"name":117,"username":118,"avatarUrl":52,"headline":119},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":66,"name":122},"JavaScript",[124,125,126,127],{"slug":62,"name":63,"color":52},{"slug":66,"name":67,"color":52},{"slug":86,"name":87,"color":52},{"slug":70,"name":71,"color":52},{"assessments":129},1,{"slug":46,"title":131},"bfcache eligibility — interactive playground",{"blocks":133,"version":129},[134,138,141,146,149,152,155,158,161,167,170,173,176,179,182,186,189,193,196,199,202,205,208,211,214,218,221,224,227,230,233,236,239,242,245,248,251,254,261,264,267,270,273],{"id":135,"html":136,"type":137},"b1","\u003Cp>Hit the back button on most sites and the previous page just reappears — no spinner, no white flash, no network tab lighting up. It was never gone. The browser froze it in memory instead of throwing it away.\u003C\u002Fp>","paragraph",{"id":139,"html":140,"type":137},"b2","\u003Cp>Then one day, on your own app, it stops being instant. Same page, same back button, now with a full reload every time. Nobody touched the router. Somebody added a &quot;you have unsaved changes&quot; warning.\u003C\u002Fp>",{"id":142,"html":143,"text":144,"type":145,"level":31},"b3","What &quot;instant&quot; actually is","What \"instant\" actually is","heading",{"id":147,"html":148,"type":137},"b4","\u003Cp>That instant restore has a name: the back\u002Fforward cache, or bfcache. When you navigate away, instead of destroying the page, the browser can suspend it whole — DOM, JavaScript heap, running timers, scroll position, all of it — and hand it straight back if you hit back or forward. No HTML re-parse, no JS re-execution, no re-fetch. It just un-pauses.\u003C\u002Fp>",{"id":150,"html":151,"type":137},"b5","\u003Cp>Safari and Firefox have shipped some form of this for a very long time. Chrome was the holdout — for years it only cached in narrow cases — until it rolled out broad bfcache support starting with Chrome 96 in late 2021. Today, on a site that qualifies, back and forward navigation across all three engines can be effectively instant.\u003C\u002Fp>",{"id":153,"html":154,"type":137},"b6","\u003Cp>&quot;On a site that qualifies&quot; is doing a lot of work in that sentence. Plenty of real apps don&#39;t, and most of the time nobody notices, because nothing announces the miss. The page just... loads normally, the way it always has. You only notice the regression if you had bfcache and lost it.\u003C\u002Fp>",{"id":156,"html":157,"text":157,"type":145,"level":31},"b7","The listener that looks harmless",{"id":159,"html":160,"type":137},"b8","\u003Cp>Say your app has a form, and you don&#39;t want people losing work by fat-fingering the back button. The standard move:\u003C\u002Fp>",{"id":162,"code":163,"type":164,"language":165,"highlight":166},"b9","window.addEventListener('beforeunload', (e) => {\n  if (hasUnsavedChanges) {\n    e.preventDefault();\n    e.returnValue = '';\n  }\n});","code","js",[],{"id":168,"html":169,"type":137},"b10","\u003Cp>Reasonable. It only shows the native &quot;leave site?&quot; prompt when \u003Ccode>hasUnsavedChanges\u003C\u002Fcode> is true. Surely an idle listener that does nothing most of the time can&#39;t cost anything?\u003C\u002Fp>",{"id":171,"html":172,"type":137},"b11","\u003Cp>For a long time, in the browsers that enforced this most strictly, it could. The check happened at navigation time, before your callback ever ran — so the browser couldn&#39;t know whether \u003Cem>this\u003C\u002Fem> departure was one where the handler would actually intervene. It only knew a \u003Ccode>beforeunload\u003C\u002Fcode> handler existed, and an always-mounted one is a standing promise the page might need to act on the way out, which a suspended, un-runnable page can&#39;t keep. So the listener being registered at all — not what it did on any given visit — was what counted against you.\u003C\u002Fp>",{"id":174,"html":175,"type":137},"b12","\u003Cp>That specific rule has been shifting. Some engines have relaxed how much a mere \u003Ccode>beforeunload\u003C\u002Fcode> registration costs you; others still treat it as a real risk. Which means you can&#39;t safely assume either behavior for the whole spread of browsers and versions your users are actually running — and the fix costs nothing regardless of which way today&#39;s browser leans.\u003C\u002Fp>",{"id":177,"html":178,"type":137},"b13","\u003Cp>\u003Ccode>unload\u003C\u002Fcode> doesn&#39;t get the benefit of that ambiguity: it&#39;s the one still widely documented as a reliable disqualifier, in some engines for the page&#39;s whole lifetime, listener present or not, browser-relaxation or not.\u003C\u002Fp>",{"id":180,"html":181,"type":137},"b14","\u003Cp>Open Chrome DevTools → Application → Back\u002Fforward cache, and click &quot;Test back\u002Fforward cache.&quot; It runs the browser&#39;s own eligibility check and names the specific not-restored reasons for \u003Cem>your\u003C\u002Fem> page, on \u003Cem>your\u003C\u002Fem> browser version — which beats guessing from an article, this one included.\u003C\u002Fp>",{"id":183,"html":184,"text":185,"type":145,"level":31},"b15","The fix isn&#39;t deleting the warning","The fix isn't deleting the warning",{"id":187,"html":188,"type":137},"b16","\u003Cp>The warning is legitimate — you don&#39;t want to silently lose someone&#39;s draft. What&#39;s fixable is \u003Cem>when the listener exists\u003C\u002Fem>, not whether it does.\u003C\u002Fp>",{"id":190,"code":191,"type":164,"language":165,"highlight":192},"b17","function updateUnsavedGuard(hasUnsavedChanges) {\n  if (hasUnsavedChanges) {\n    window.addEventListener('beforeunload', warnOnLeave);\n  } else {\n    window.removeEventListener('beforeunload', warnOnLeave);\n  }\n}\n\nfunction warnOnLeave(e) {\n  e.preventDefault();\n  e.returnValue = '';\n}",[],{"id":194,"html":195,"type":137},"b18","\u003Cp>Attach it only while there&#39;s something to lose, and detach it the moment the draft is saved. A page with no unsaved changes has no listener mounted, so it&#39;s eligible for bfcache the rest of the time — which, for most forms, is nearly always.\u003C\u002Fp>",{"id":197,"html":198,"type":137},"b19","\u003C!-- playground:start -->",{"id":200,"html":201,"text":201,"type":145,"level":31},"b20","🎮 Try it yourself",{"id":203,"html":204,"type":137},"b21","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fbfcache-beforeunload-back-button\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":206,"html":207,"type":137},"b22","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":209,"html":210,"type":137},"b23","\u003C!-- playground:end -->",{"id":212,"html":213,"type":137},"b24","\u003Cp>For cleanup that isn&#39;t about a warning dialog — closing a WebSocket, cancelling a poll, flushing analytics — reach for \u003Ccode>pagehide\u003C\u002Fcode> instead of \u003Ccode>unload\u003C\u002Fcode>. \u003Ccode>unload\u003C\u002Fcode> is the one without the ambiguity: some engines disqualify a page from bfcache just for having an \u003Ccode>unload\u003C\u002Fcode> listener anywhere on the page, for its whole lifetime, warning or not.\u003C\u002Fp>",{"id":215,"code":216,"type":164,"language":165,"highlight":217},"b25","window.addEventListener('pagehide', (event) => {\n  socket.close();\n  clearInterval(pollTimer);\n  \u002F\u002F event.persisted === true means the page is being frozen for bfcache,\n  \u002F\u002F not destroyed — the same handler covers both cases safely.\n});\n\nwindow.addEventListener('pageshow', (event) => {\n  if (event.persisted) {\n    \u002F\u002F restored from bfcache, not a fresh load — the socket you closed\n    \u002F\u002F in pagehide is gone, so reopen it and refresh anything time-sensitive.\n    socket = reconnect();\n    refreshStaleTimestamps();\n  }\n});",[],{"id":219,"html":220,"type":137},"b26","\u003Cp>\u003Ccode>pageshow\u003C\u002Fcode> fires on \u003Cem>every\u003C\u002Fem> page load, fresh or restored — \u003Ccode>event.persisted\u003C\u002Fcode> is the flag that tells you which one just happened. That&#39;s the piece most cleanup code is missing: it assumes a fresh mount and re-runs setup that a restored page already has, or it never notices the page came back at all and sits there with a closed socket and a timestamp from ten minutes ago.\u003C\u002Fp>",{"id":222,"html":223,"text":223,"type":145,"level":31},"b27","What you get back",{"id":225,"html":226,"type":137},"b28","\u003Cp>Fix both, and the back button goes back to being boring: instant, no flash, no waterfall in the network tab. The unsaved-changes prompt still fires when it should — it&#39;s just not paying rent on every page for the whole time your form is untouched. And a restored tab reconnects its socket and refreshes its stale data instead of quietly pretending nothing happened while it was away.\u003C\u002Fp>",{"id":228,"html":229,"type":137},"b29","\u003Cp>One more worth a mention, since it&#39;s not a listener at all: a \u003Ccode>Cache-Control: no-store\u003C\u002Fcode> header used to disqualify a page from bfcache outright, no JavaScript involved. Chrome has since relaxed that — a \u003Ccode>no-store\u003C\u002Fcode> page can now enter bfcache as long as the user&#39;s cookies and \u003Ccode>Authorization\u003C\u002Fcode> state don&#39;t change while it&#39;s parked (either one evicts it immediately, and it gets a shorter hold than a normal bfcache entry regardless), though a \u003Ccode>no-store\u003C\u002Fcode> page that also keeps a WebSocket, WebTransport, or WebRTC connection open still gets excluded outright. Outside the \u003Ccode>no-store\u003C\u002Fcode> case, Chrome has separately stopped treating an open WebSocket as an automatic blocker on its own — since Chrome 149, it closes the socket on the way out and hands you an \u003Ccode>onclose\u003C\u002Fcode>\u002F\u003Ccode>pageshow\u003C\u002Fcode> to reconnect from, instead of refusing to cache the page at all. WebTransport and WebRTC didn&#39;t get the same carve-out; they still block bfcache outright, with or without \u003Ccode>no-store\u003C\u002Fcode>. All of this is Chrome-specific and recent, so don&#39;t assume it holds in every engine or for every connection type — if your listeners are clean and bfcache still won&#39;t take, check this header (and what&#39;s still open) and verify the current behavior in DevTools rather than trusting any rule, old or new, by default.\u003C\u002Fp>",{"id":231,"html":232,"type":137},"b30","\u003C!-- quiz:start -->",{"id":234,"html":235,"text":235,"type":145,"level":31},"b31","🧠 Test yourself",{"id":237,"html":238,"type":137},"b32","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fbfcache-beforeunload-back-button\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":240,"html":241,"type":137},"b33","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":243,"html":244,"type":137},"b34","\u003C!-- quiz:end -->",{"id":246,"html":247,"type":137},"b35","\u003Cp>Open DevTools → Application → Back\u002Fforward cache on your own site and click &quot;Test back\u002Fforward cache.&quot; What comes back — and were you expecting it?\u003C\u002Fp>",{"id":249,"html":250,"type":137},"b36","\u003C!-- related:start -->",{"id":252,"html":253,"text":253,"type":145,"level":31},"b37","📚 Read next",{"id":255,"type":256,"items":257,"ordered":18},"b38","list",[258,259,260],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Frequestidlecallback-can-wait-forever\">The Background Task That Waited 40 Seconds for &#39;Idle&#39;\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcrypto-randomuuid-native-uuid\">Stop importing \u003Ccode>uuid\u003C\u002Fcode>. \u003Ccode>crypto.randomUUID()\u003C\u002Fcode> has been native since 2021.\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Findexeddb-localstorage-main-thread-blocking\">localStorage Isn&#39;t Free — It&#39;s Blocking Your Main Thread\u003C\u002Fa>",{"id":262,"html":263,"type":137},"b39","\u003C!-- related:end -->",{"id":265,"type":266},"b40","divider",{"id":268,"html":269,"type":137},"b41","\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":271,"html":272,"type":137},"b42","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":274,"type":256,"items":275,"ordered":18},"b43",[276,277,278],"⭐ \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>","Hit the back button on most sites and the previous page just reappears — no spinner, no white flash, no network tab lighting up. It was never gone. The browser froze it in memory instead of throwing it away.\n\nThen one day, on your own app, it stops being instant. Same page, same back button, now with a full reload every time. Nobody touched the router. Somebody added a \"you have unsaved changes\" warning.\n\n## What \"instant\" actually is\n\nThat instant restore has a name: the back\u002Fforward cache, or bfcache. When you navigate away, instead of destroying the page, the browser can suspend it whole — DOM, JavaScript heap, running timers, scroll position, all of it — and hand it straight back if you hit back or forward. No HTML re-parse, no JS re-execution, no re-fetch. It just un-pauses.\n\nSafari and Firefox have shipped some form of this for a very long time. Chrome was the holdout — for years it only cached in narrow cases — until it rolled out broad bfcache support starting with Chrome 96 in late 2021. Today, on a site that qualifies, back and forward navigation across all three engines can be effectively instant.\n\n\"On a site that qualifies\" is doing a lot of work in that sentence. Plenty of real apps don't, and most of the time nobody notices, because nothing announces the miss. The page just... loads normally, the way it always has. You only notice the regression if you had bfcache and lost it.\n\n## The listener that looks harmless\n\nSay your app has a form, and you don't want people losing work by fat-fingering the back button. The standard move:\n\n```js\nwindow.addEventListener('beforeunload', (e) => {\n  if (hasUnsavedChanges) {\n    e.preventDefault();\n    e.returnValue = '';\n  }\n});\n```\n\nReasonable. It only shows the native \"leave site?\" prompt when `hasUnsavedChanges` is true. Surely an idle listener that does nothing most of the time can't cost anything?\n\nFor a long time, in the browsers that enforced this most strictly, it could. The check happened at navigation time, before your callback ever ran — so the browser couldn't know whether *this* departure was one where the handler would actually intervene. It only knew a `beforeunload` handler existed, and an always-mounted one is a standing promise the page might need to act on the way out, which a suspended, un-runnable page can't keep. So the listener being registered at all — not what it did on any given visit — was what counted against you.\n\nThat specific rule has been shifting. Some engines have relaxed how much a mere `beforeunload` registration costs you; others still treat it as a real risk. Which means you can't safely assume either behavior for the whole spread of browsers and versions your users are actually running — and the fix costs nothing regardless of which way today's browser leans.\n\n`unload` doesn't get the benefit of that ambiguity: it's the one still widely documented as a reliable disqualifier, in some engines for the page's whole lifetime, listener present or not, browser-relaxation or not.\n\nOpen Chrome DevTools → Application → Back\u002Fforward cache, and click \"Test back\u002Fforward cache.\" It runs the browser's own eligibility check and names the specific not-restored reasons for *your* page, on *your* browser version — which beats guessing from an article, this one included.\n\n## The fix isn't deleting the warning\n\nThe warning is legitimate — you don't want to silently lose someone's draft. What's fixable is *when the listener exists*, not whether it does.\n\n```js\nfunction updateUnsavedGuard(hasUnsavedChanges) {\n  if (hasUnsavedChanges) {\n    window.addEventListener('beforeunload', warnOnLeave);\n  } else {\n    window.removeEventListener('beforeunload', warnOnLeave);\n  }\n}\n\nfunction warnOnLeave(e) {\n  e.preventDefault();\n  e.returnValue = '';\n}\n```\n\nAttach it only while there's something to lose, and detach it the moment the draft is saved. A page with no unsaved changes has no listener mounted, so it's eligible for bfcache the rest of the time — which, for most forms, is nearly always.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fbfcache-beforeunload-back-button\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\nFor cleanup that isn't about a warning dialog — closing a WebSocket, cancelling a poll, flushing analytics — reach for `pagehide` instead of `unload`. `unload` is the one without the ambiguity: some engines disqualify a page from bfcache just for having an `unload` listener anywhere on the page, for its whole lifetime, warning or not.\n\n```js\nwindow.addEventListener('pagehide', (event) => {\n  socket.close();\n  clearInterval(pollTimer);\n  \u002F\u002F event.persisted === true means the page is being frozen for bfcache,\n  \u002F\u002F not destroyed — the same handler covers both cases safely.\n});\n\nwindow.addEventListener('pageshow', (event) => {\n  if (event.persisted) {\n    \u002F\u002F restored from bfcache, not a fresh load — the socket you closed\n    \u002F\u002F in pagehide is gone, so reopen it and refresh anything time-sensitive.\n    socket = reconnect();\n    refreshStaleTimestamps();\n  }\n});\n```\n\n`pageshow` fires on *every* page load, fresh or restored — `event.persisted` is the flag that tells you which one just happened. That's the piece most cleanup code is missing: it assumes a fresh mount and re-runs setup that a restored page already has, or it never notices the page came back at all and sits there with a closed socket and a timestamp from ten minutes ago.\n\n## What you get back\n\nFix both, and the back button goes back to being boring: instant, no flash, no waterfall in the network tab. The unsaved-changes prompt still fires when it should — it's just not paying rent on every page for the whole time your form is untouched. And a restored tab reconnects its socket and refreshes its stale data instead of quietly pretending nothing happened while it was away.\n\nOne more worth a mention, since it's not a listener at all: a `Cache-Control: no-store` header used to disqualify a page from bfcache outright, no JavaScript involved. Chrome has since relaxed that — a `no-store` page can now enter bfcache as long as the user's cookies and `Authorization` state don't change while it's parked (either one evicts it immediately, and it gets a shorter hold than a normal bfcache entry regardless), though a `no-store` page that also keeps a WebSocket, WebTransport, or WebRTC connection open still gets excluded outright. Outside the `no-store` case, Chrome has separately stopped treating an open WebSocket as an automatic blocker on its own — since Chrome 149, it closes the socket on the way out and hands you an `onclose`\u002F`pageshow` to reconnect from, instead of refusing to cache the page at all. WebTransport and WebRTC didn't get the same carve-out; they still block bfcache outright, with or without `no-store`. All of this is Chrome-specific and recent, so don't assume it holds in every engine or for every connection type — if your listeners are clean and bfcache still won't take, check this header (and what's still open) and verify the current behavior in DevTools rather than trusting any rule, old or new, by default.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fbfcache-beforeunload-back-button\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\nOpen DevTools → Application → Back\u002Fforward cache on your own site and click \"Test back\u002Fforward cache.\" What comes back — and were you expecting it?\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [The Background Task That Waited 40 Seconds for 'Idle'](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Frequestidlecallback-can-wait-forever)\n- [Stop importing `uuid`. `crypto.randomUUID()` has been native since 2021.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcrypto-randomuuid-native-uuid)\n- [localStorage Isn't Free — It's Blocking Your Main Thread](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Findexeddb-localstorage-main-thread-blocking)\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":109,"canonical":281,"description":282},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fbfcache-beforeunload-back-button","A `beforeunload` listener you left mounted forever can still quietly cost you bfcache eligibility — and an `unload` listener reliably will. Here's the pagehide\u002Fpageshow fix, and wh","01a0f0ef-d9b9-7138-ac2c-8de58bcb0dac",{"id":285,"locked":18},"01a0f0ef-d9df-749c-9866-58cefc477f56",[287],{"id":45,"slug":46,"title":48,"_count":288},{"questions":51},[290],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":292,"questionCount":51},{"questions":51}]