[{"data":1,"prerenderedAt":1237},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Freplacing-unload-handlers-for-bfcache":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Freplacing-unload-handlers-for-bfcache":1228},{"id":4,"title":5,"body":6,"description":1208,"extension":1209,"meta":1210,"navigation":1221,"path":1222,"seo":1223,"stem":1226,"__hash__":1227},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Freplacing-unload-handlers-for-bfcache\u002Findex.md","Replacing unload Handlers for bfcache",{"type":7,"value":8,"toc":1190},"minimark",[9,14,40,59,230,235,278,282,290,296,302,311,416,420,425,556,559,563,668,671,675,681,685,691,778,782,789,918,922,929,933,945,949,988,992,1003,1015,1027,1041,1045,1067,1071,1089,1104,1121,1136,1148,1152,1175,1180,1183,1186],[10,11,13],"h1",{"id":12},"how-to-replace-unload-handlers-so-pages-can-use-bfcache","How to Replace unload Handlers So Pages Can Use bfcache",[15,16,17,18,23,24,28,29,33,34,36,37,39],"p",{},"This guide covers the single most common bfcache blocker, within ",[19,20,22],"a",{"href":21},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002F","Back\u002FForward Cache (bfcache)"," in ",[19,25,27],{"href":26},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",". The ",[30,31,32],"code",{},"unload"," event has two problems. It makes pages ineligible for the back\u002Fforward cache in Chromium desktop and Firefox, because code written for ",[30,35,32],{}," assumes the page is gone forever. And it is unreliable: on mobile, pages are frequently discarded or the app is backgrounded without ",[30,38,32],{}," ever firing, so the analytics and cleanup code people put there often never runs.",[15,41,42,43,46,47,50,51,54,55,58],{},"The replacements are better on both counts. ",[30,44,45],{},"visibilitychange"," (when the state becomes ",[30,48,49],{},"hidden",") fires whenever the user leaves the page — switching tabs, backgrounding the app, navigating away — and is the most reliable last chance to send data. ",[30,52,53],{},"pagehide"," fires when the page is being navigated away from, including when it enters bfcache, with ",[30,56,57],{},"event.persisted"," telling you which. Neither blocks bfcache.",[15,60,61],{},[62,63,69,70,69,77,69,81,69,84,69,93,69,99,69,105,69,112,69,116,69,121,69,124,69,128,69,131,69,135,69,139,69,142,69,147,69,151,69,153,69,156,69,158,69,161,69,164,69,168,69,170,69,172,69,174,69,177,69,179,69,182,69,185,69,188,69,190,69,193,69,197,69,199,69,201,69,204,69,208,69,212,69,214,69,216,69,218,69,220,69,222,69,226,69],"svg",{"viewBox":64,"width":65,"role":66,"ariaLabel":67,"style":68},"0 0 760 244","100%","img","Comparison of unload, beforeunload, pagehide and visibilitychange by reliability on mobile and effect on bfcache.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[71,72],"rect",{"className":73,"x":75,"y":75,"width":65,"height":65,"fill":76},[74],"svg-canvas","0","#ffffff",[78,79,80],"title",{},"Page lifecycle events compared",[82,83,67],"desc",{},[71,85],{"x":86,"y":86,"width":87,"height":88,"rx":89,"fill":90,"stroke":91,"style":92},"1","758","242","10","none","currentColor","stroke-opacity:0.18",[94,95,80],"text",{"x":96,"y":97,"fill":91,"style":98},"28.0","34.0","font-size:16px;font-weight:700",[71,100],{"x":96,"y":101,"width":102,"height":103,"rx":75,"fill":91,"stroke":91,"style":104},"56.0","161.3","30.0","fill-opacity:0.06;stroke-opacity:0.4",[94,106,111],{"x":107,"y":108,"fill":91,"style":109,"textAnchor":110},"38.0","75.5","font-size:12.5px;font-weight:700","start","Event",[71,113],{"x":114,"y":101,"width":115,"height":103,"rx":75,"fill":91,"stroke":91,"style":104},"189.3","180.9",[94,117,120],{"x":118,"y":108,"fill":91,"style":109,"textAnchor":119},"279.7","middle","Fires reliably on mobile",[71,122],{"x":123,"y":101,"width":115,"height":103,"rx":75,"fill":91,"stroke":91,"style":104},"370.2",[94,125,127],{"x":126,"y":108,"fill":91,"style":109,"textAnchor":119},"460.6","Blocks bfcache",[71,129],{"x":130,"y":101,"width":115,"height":103,"rx":75,"fill":91,"stroke":91,"style":104},"551.1",[94,132,134],{"x":133,"y":108,"fill":91,"style":109,"textAnchor":119},"641.5","Use for",[71,136],{"x":96,"y":137,"width":102,"height":103,"rx":75,"fill":90,"stroke":91,"style":138},"86.0","stroke-opacity:0.35",[94,140,32],{"x":107,"y":141,"fill":91,"style":109,"textAnchor":110},"105.5",[71,143],{"x":114,"y":137,"width":115,"height":103,"rx":75,"fill":144,"stroke":145,"style":146},"#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[94,148,150],{"x":118,"y":141,"fill":91,"style":149,"textAnchor":119},"font-size:12px","no",[71,152],{"x":123,"y":137,"width":115,"height":103,"rx":75,"fill":144,"stroke":145,"style":146},[94,154,155],{"x":126,"y":141,"fill":91,"style":149,"textAnchor":119},"yes",[71,157],{"x":130,"y":137,"width":115,"height":103,"rx":75,"fill":90,"stroke":91,"style":138},[94,159,160],{"x":133,"y":141,"fill":91,"style":149,"textAnchor":119},"nothing — remove it",[71,162],{"x":96,"y":163,"width":102,"height":103,"rx":75,"fill":90,"stroke":91,"style":138},"116.0",[94,165,167],{"x":107,"y":166,"fill":91,"style":109,"textAnchor":110},"135.5","beforeunload",[71,169],{"x":114,"y":163,"width":115,"height":103,"rx":75,"fill":144,"stroke":145,"style":146},[94,171,150],{"x":118,"y":166,"fill":91,"style":149,"textAnchor":119},[71,173],{"x":123,"y":163,"width":115,"height":103,"rx":75,"fill":91,"stroke":91,"style":104},[94,175,176],{"x":126,"y":166,"fill":91,"style":149,"textAnchor":119},"in some cases",[71,178],{"x":130,"y":163,"width":115,"height":103,"rx":75,"fill":90,"stroke":91,"style":138},[94,180,181],{"x":133,"y":166,"fill":91,"style":149,"textAnchor":119},"unsaved-changes prompt only",[71,183],{"x":96,"y":184,"width":102,"height":103,"rx":75,"fill":90,"stroke":91,"style":138},"146.0",[94,186,53],{"x":107,"y":187,"fill":91,"style":109,"textAnchor":110},"165.5",[71,189],{"x":114,"y":184,"width":115,"height":103,"rx":75,"fill":91,"stroke":91,"style":104},[94,191,192],{"x":118,"y":187,"fill":91,"style":149,"textAnchor":119},"mostly",[71,194],{"x":123,"y":184,"width":115,"height":103,"rx":75,"fill":195,"stroke":195,"style":196},"#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[94,198,150],{"x":126,"y":187,"fill":91,"style":149,"textAnchor":119},[71,200],{"x":130,"y":184,"width":115,"height":103,"rx":75,"fill":90,"stroke":91,"style":138},[94,202,203],{"x":133,"y":187,"fill":91,"style":149,"textAnchor":119},"cleanup that can be undone",[71,205],{"x":96,"y":206,"width":102,"height":207,"rx":75,"fill":90,"stroke":91,"style":138},"176.0","46.0",[94,209,211],{"x":107,"y":210,"fill":91,"style":109,"textAnchor":110},"203.5","visibilitychange → hidden",[71,213],{"x":114,"y":206,"width":115,"height":207,"rx":75,"fill":195,"stroke":195,"style":196},[94,215,155],{"x":118,"y":210,"fill":91,"style":149,"textAnchor":119},[71,217],{"x":123,"y":206,"width":115,"height":207,"rx":75,"fill":195,"stroke":195,"style":196},[94,219,150],{"x":126,"y":210,"fill":91,"style":149,"textAnchor":119},[71,221],{"x":130,"y":206,"width":115,"height":207,"rx":75,"fill":90,"stroke":91,"style":138},[94,223,225],{"x":133,"y":224,"fill":91,"style":149,"textAnchor":119},"195.5","sending analytics and saving",[94,227,229],{"x":133,"y":228,"fill":91,"style":149,"textAnchor":119},"211.5","state",[231,232,234],"h2",{"id":233},"rapid-diagnosis","Rapid Diagnosis",[236,237,238,250,263,269],"ul",{},[239,240,241,245,246,249],"li",{},[242,243,244],"strong",{},"Find listeners."," In DevTools Console, ",[30,247,248],{},"getEventListeners(window).unload"," lists registered handlers (Chromium); the Elements panel's Event Listeners tab shows them with source links.",[239,251,252,255,256,258,259,262],{},[242,253,254],{},"Search code and vendors"," for ",[30,257,32],{},", ",[30,260,261],{},"onunload",", and libraries known to use it in older versions (some analytics, session replay and A\u002FB tools).",[239,264,265,268],{},[242,266,267],{},"Check the bfcache test."," DevTools reports \"unload handler\" with the frame that registered it.",[239,270,271,274,275,277],{},[242,272,273],{},"Check analytics completeness."," If session-end events are missing for many mobile sessions, they are probably sent from ",[30,276,32],{},".",[231,279,281],{"id":280},"root-cause-analysis","Root Cause Analysis",[15,283,284,287,288,277],{},[242,285,286],{},"1. Analytics flushing."," The classic use: send buffered events or a session-end beacon in ",[30,289,32],{},[15,291,292,295],{},[242,293,294],{},"2. Cleanup."," Closing connections, releasing locks, clearing timers — work that is pointless on a real unload and harmful if the page is restored.",[15,297,298,301],{},[242,299,300],{},"3. Saving state."," Writing form drafts or UI state to storage on exit.",[15,303,304,307,308,310],{},[242,305,306],{},"4. Third-party defaults."," Vendor snippets added years ago that register ",[30,309,32],{}," without your knowledge.",[15,312,313],{},[62,314,69,317,69,320,69,323,69,325,69,328,69,330,69,334,69,340,69,346,69,350,69,354,69,358,69,362,69,366,69,371,69,374,69,381,69,387,69,391,69,395,69,399,69,403,69,407,69,411,69],{"viewBox":315,"width":65,"role":66,"ariaLabel":316,"style":68},"0 0 760 204","Timeline of lifecycle events when a mobile user switches apps and the page is later discarded, showing unload never firing.",[71,318],{"className":319,"x":75,"y":75,"width":65,"height":65,"fill":76},[74],[78,321,322],{},"Which events fire when a mobile user leaves",[82,324,316],{},[71,326],{"x":86,"y":86,"width":87,"height":327,"rx":89,"fill":90,"stroke":91,"style":92},"202",[94,329,322],{"x":96,"y":97,"fill":91,"style":98},[94,331,333],{"x":96,"y":332,"fill":91,"style":109},"74.0","User",[71,335],{"x":336,"y":101,"width":337,"height":338,"rx":339,"fill":91,"stroke":91,"style":104},"85.0","126.8","26.0","3",[94,341,345],{"x":342,"y":343,"fill":91,"style":344,"textAnchor":119},"148.5","73.5","font-size:11.5px","reading page",[71,347],{"x":348,"y":101,"width":349,"height":338,"rx":339,"fill":91,"stroke":91,"style":104},"213.4","511.9",[94,351,353],{"x":352,"y":343,"fill":91,"style":344,"textAnchor":119},"469.3","in another app",[94,355,357],{"x":96,"y":356,"fill":91,"style":109},"112.0","Events",[71,359],{"x":348,"y":360,"width":361,"height":338,"rx":339,"fill":144,"stroke":145,"style":146},"94.0","19.9",[71,363],{"x":364,"y":360,"width":365,"height":338,"rx":339,"fill":91,"stroke":91,"style":104},"234.8","383.5",[94,367,370],{"x":368,"y":369,"fill":91,"style":344,"textAnchor":119},"426.5","111.5","page frozen",[71,372],{"x":373,"y":360,"width":361,"height":338,"rx":339,"fill":144,"stroke":145,"style":146},"619.8",[375,376],"line",{"x1":377,"y1":378,"x2":379,"y2":378,"stroke":91,"style":380},"84.3","134.0","726.0","stroke-opacity:0.4",[94,382,386],{"x":383,"y":384,"fill":385,"style":344,"textAnchor":119},"90.3","151.0","#51617a","0s",[94,388,390],{"x":389,"y":384,"fill":385,"style":344,"textAnchor":119},"191.2","10s",[94,392,394],{"x":393,"y":384,"fill":385,"style":344,"textAnchor":119},"298.2","20s",[94,396,398],{"x":397,"y":384,"fill":385,"style":344,"textAnchor":119},"405.1","30s",[94,400,402],{"x":401,"y":384,"fill":385,"style":344,"textAnchor":119},"512.1","40s",[94,404,406],{"x":405,"y":384,"fill":385,"style":344,"textAnchor":119},"619.0","50s",[94,408,410],{"x":409,"y":384,"fill":385,"style":344,"textAnchor":119},"723.0","60s",[94,412,415],{"x":96,"y":413,"fill":385,"style":414},"182.0","font-size:12.5px","visibilitychange (hidden) fires at 12s; at 50s the tab is discarded without unload ever firing.",[231,417,419],{"id":418},"step-by-step-resolution","Step-by-Step Resolution",[421,422,424],"h3",{"id":423},"_1-send-analytics-on-visibilitychange-with-sendbeacon","1. Send analytics on visibilitychange with sendBeacon",[426,427,432],"pre",{"className":428,"code":429,"language":430,"meta":431,"style":431},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","addEventListener('visibilitychange', () => {\n  if (document.visibilityState === 'hidden' && queue.length) {\n    navigator.sendBeacon('\u002Fanalytics', JSON.stringify(queue.splice(0)));\n  }\n});\n\u002F\u002F trade-off: visibilitychange fires on every tab switch, so a long visit may\n\u002F\u002F send several beacons. De-duplicate server-side by event id rather than\n\u002F\u002F trying to send exactly once from the client.\n","javascript","",[30,433,434,460,488,525,531,537,544,550],{"__ignoreMap":431},[435,436,438,442,446,450,453,457],"span",{"class":375,"line":437},1,[435,439,441],{"class":440},"smZ65","addEventListener",[435,443,445],{"class":444},"saISM","(",[435,447,449],{"class":448},"sZ8jY","'visibilitychange'",[435,451,452],{"class":444},", () ",[435,454,456],{"class":455},"sPARh","=>",[435,458,459],{"class":444}," {\n",[435,461,463,466,469,472,475,478,481,485],{"class":375,"line":462},2,[435,464,465],{"class":455},"  if",[435,467,468],{"class":444}," (document.visibilityState ",[435,470,471],{"class":455},"===",[435,473,474],{"class":448}," 'hidden'",[435,476,477],{"class":455}," &&",[435,479,480],{"class":444}," queue.",[435,482,484],{"class":483},"sPXB4","length",[435,486,487],{"class":444},") {\n",[435,489,491,494,497,499,502,504,507,509,512,515,518,520,522],{"class":375,"line":490},3,[435,492,493],{"class":444},"    navigator.",[435,495,496],{"class":440},"sendBeacon",[435,498,445],{"class":444},[435,500,501],{"class":448},"'\u002Fanalytics'",[435,503,258],{"class":444},[435,505,506],{"class":483},"JSON",[435,508,277],{"class":444},[435,510,511],{"class":440},"stringify",[435,513,514],{"class":444},"(queue.",[435,516,517],{"class":440},"splice",[435,519,445],{"class":444},[435,521,75],{"class":483},[435,523,524],{"class":444},")));\n",[435,526,528],{"class":375,"line":527},4,[435,529,530],{"class":444},"  }\n",[435,532,534],{"class":375,"line":533},5,[435,535,536],{"class":444},"});\n",[435,538,540],{"class":375,"line":539},6,[435,541,543],{"class":542},"sjfSM","\u002F\u002F trade-off: visibilitychange fires on every tab switch, so a long visit may\n",[435,545,547],{"class":375,"line":546},7,[435,548,549],{"class":542},"\u002F\u002F send several beacons. De-duplicate server-side by event id rather than\n",[435,551,553],{"class":375,"line":552},8,[435,554,555],{"class":542},"\u002F\u002F trying to send exactly once from the client.\n",[15,557,558],{},"Expected outcome: more complete data on mobile and no bfcache block.",[421,560,562],{"id":561},"_2-move-cleanup-to-pagehide-and-make-it-reversible","2. Move cleanup to pagehide and make it reversible",[426,564,566],{"className":428,"code":565,"language":430,"meta":431,"style":431},"addEventListener('pagehide', (event) => {\n  stopPolling();\n  if (event.persisted) {\n    \u002F\u002F Page may be restored: keep in-memory state, just pause activity.\n  }\n});\naddEventListener('pageshow', (event) => { if (event.persisted) startPolling(); });\n\u002F\u002F trade-off: reversible cleanup is more code than fire-and-forget teardown.\n\u002F\u002F The alternative — not cleaning up at all — is fine for most pages, because\n\u002F\u002F the browser tears everything down on a real unload anyway.\n",[30,567,568,591,599,606,611,615,619,651,656,662],{"__ignoreMap":431},[435,569,570,572,574,577,580,584,587,589],{"class":375,"line":437},[435,571,441],{"class":440},[435,573,445],{"class":444},[435,575,576],{"class":448},"'pagehide'",[435,578,579],{"class":444},", (",[435,581,583],{"class":582},"sQw3B","event",[435,585,586],{"class":444},") ",[435,588,456],{"class":455},[435,590,459],{"class":444},[435,592,593,596],{"class":375,"line":462},[435,594,595],{"class":440},"  stopPolling",[435,597,598],{"class":444},"();\n",[435,600,601,603],{"class":375,"line":490},[435,602,465],{"class":455},[435,604,605],{"class":444}," (event.persisted) {\n",[435,607,608],{"class":375,"line":527},[435,609,610],{"class":542},"    \u002F\u002F Page may be restored: keep in-memory state, just pause activity.\n",[435,612,613],{"class":375,"line":533},[435,614,530],{"class":444},[435,616,617],{"class":375,"line":539},[435,618,536],{"class":444},[435,620,621,623,625,628,630,632,634,636,639,642,645,648],{"class":375,"line":546},[435,622,441],{"class":440},[435,624,445],{"class":444},[435,626,627],{"class":448},"'pageshow'",[435,629,579],{"class":444},[435,631,583],{"class":582},[435,633,586],{"class":444},[435,635,456],{"class":455},[435,637,638],{"class":444}," { ",[435,640,641],{"class":455},"if",[435,643,644],{"class":444}," (event.persisted) ",[435,646,647],{"class":440},"startPolling",[435,649,650],{"class":444},"(); });\n",[435,652,653],{"class":375,"line":552},[435,654,655],{"class":542},"\u002F\u002F trade-off: reversible cleanup is more code than fire-and-forget teardown.\n",[435,657,659],{"class":375,"line":658},9,[435,660,661],{"class":542},"\u002F\u002F The alternative — not cleaning up at all — is fine for most pages, because\n",[435,663,665],{"class":375,"line":664},10,[435,666,667],{"class":542},"\u002F\u002F the browser tears everything down on a real unload anyway.\n",[15,669,670],{},"Expected outcome: cleanup happens without blocking restoration, and restored pages resume.",[421,672,674],{"id":673},"_3-save-state-continuously-not-on-exit","3. Save state continuously, not on exit",[15,676,677,678,680],{},"Write drafts and UI state when they change (debounced) or on ",[30,679,45],{},", instead of relying on an exit event.",[421,682,684],{"id":683},"_4-use-beforeunload-only-conditionally","4. Use beforeunload only conditionally",[15,686,687,688,690],{},"If you need an \"unsaved changes\" prompt, add the ",[30,689,167],{}," listener only while there are unsaved changes and remove it as soon as they are saved.",[426,692,694],{"className":428,"code":693,"language":430,"meta":431,"style":431},"function onDirtyChange(dirty) {\n  if (dirty) addEventListener('beforeunload', warn);\n  else removeEventListener('beforeunload', warn);\n}\nfunction warn(e) { e.preventDefault(); }\n\u002F\u002F trade-off: the prompt protects users from losing work but makes that page\n\u002F\u002F ineligible for bfcache while it is dirty — acceptable, since it is rare.\n",[30,695,696,711,728,742,747,768,773],{"__ignoreMap":431},[435,697,698,701,704,706,709],{"class":375,"line":437},[435,699,700],{"class":455},"function",[435,702,703],{"class":440}," onDirtyChange",[435,705,445],{"class":444},[435,707,708],{"class":582},"dirty",[435,710,487],{"class":444},[435,712,713,715,718,720,722,725],{"class":375,"line":462},[435,714,465],{"class":455},[435,716,717],{"class":444}," (dirty) ",[435,719,441],{"class":440},[435,721,445],{"class":444},[435,723,724],{"class":448},"'beforeunload'",[435,726,727],{"class":444},", warn);\n",[435,729,730,733,736,738,740],{"class":375,"line":490},[435,731,732],{"class":455},"  else",[435,734,735],{"class":440}," removeEventListener",[435,737,445],{"class":444},[435,739,724],{"class":448},[435,741,727],{"class":444},[435,743,744],{"class":375,"line":527},[435,745,746],{"class":444},"}\n",[435,748,749,751,754,756,759,762,765],{"class":375,"line":533},[435,750,700],{"class":455},[435,752,753],{"class":440}," warn",[435,755,445],{"class":444},[435,757,758],{"class":582},"e",[435,760,761],{"class":444},") { e.",[435,763,764],{"class":440},"preventDefault",[435,766,767],{"class":444},"(); }\n",[435,769,770],{"class":375,"line":539},[435,771,772],{"class":542},"\u002F\u002F trade-off: the prompt protects users from losing work but makes that page\n",[435,774,775],{"class":375,"line":546},[435,776,777],{"class":542},"\u002F\u002F ineligible for bfcache while it is dirty — acceptable, since it is rare.\n",[421,779,781],{"id":780},"_5-block-unload-site-wide-with-permissions-policy","5. Block unload site-wide with Permissions-Policy",[15,783,784,785,788],{},"Once your code and vendors no longer rely on it, ",[30,786,787],{},"Permissions-Policy: unload=()"," prevents any frame from registering unload handlers.",[15,790,791],{},[62,792,69,795,69,798,69,801,69,803,69,806,69,808,69,814,69,819,69,822,69,825,69,829,69,833,69,835,69,839,69,843,69,846,69,850,69,854,69,857,69,861,69,865,69,872,69,876,69,880,69,884,69,889,69,891,69,894,69,898,69,901,69,904,69,907,69,911,69,914,69],{"viewBox":793,"width":65,"role":66,"ariaLabel":794,"style":68},"0 0 760 381","Five steps to remove unload handlers from a site safely.",[71,796],{"className":797,"x":75,"y":75,"width":65,"height":65,"fill":76},[74],[78,799,800],{},"unload removal checklist",[82,802,794],{},[71,804],{"x":86,"y":86,"width":87,"height":805,"rx":89,"fill":90,"stroke":91,"style":92},"379",[94,807,800],{"x":96,"y":97,"fill":91,"style":98},[71,809],{"x":810,"y":101,"width":811,"height":812,"rx":813,"fill":195,"stroke":195,"style":196},"72.0","660.0","51.0","6",[94,815,818],{"x":137,"y":816,"fill":91,"style":817,"textAnchor":110},"77.0","font-size:13px;font-weight:700","Inventory listeners",[94,820,821],{"x":137,"y":360,"fill":91,"style":149,"textAnchor":110},"Your code and every vendor snippet",[71,823],{"x":810,"y":824,"width":811,"height":812,"rx":813,"fill":195,"stroke":195,"style":196},"119.0",[94,826,828],{"x":137,"y":827,"fill":91,"style":817,"textAnchor":110},"140.0","Analytics to visibilitychange + sendBeacon",[94,830,832],{"x":137,"y":831,"fill":91,"style":149,"textAnchor":110},"157.0","Server-side de-duplication",[71,834],{"x":810,"y":413,"width":811,"height":812,"rx":813,"fill":195,"stroke":195,"style":196},[94,836,838],{"x":137,"y":837,"fill":91,"style":817,"textAnchor":110},"203.0","Cleanup to pagehide",[94,840,842],{"x":137,"y":841,"fill":91,"style":149,"textAnchor":110},"220.0","Reversible on pageshow when persisted",[71,844],{"x":810,"y":845,"width":811,"height":812,"rx":813,"fill":195,"stroke":195,"style":196},"245.0",[94,847,849],{"x":137,"y":848,"fill":91,"style":817,"textAnchor":110},"266.0","beforeunload only while dirty",[94,851,853],{"x":137,"y":852,"fill":91,"style":149,"textAnchor":110},"283.0","Added and removed with unsaved state",[71,855],{"x":810,"y":856,"width":811,"height":812,"rx":813,"fill":195,"stroke":195,"style":196},"308.0",[94,858,860],{"x":137,"y":859,"fill":91,"style":817,"textAnchor":110},"329.0","Permissions-Policy unload=()",[94,862,864],{"x":137,"y":863,"fill":91,"style":149,"textAnchor":110},"346.0","Once nothing depends on unload",[375,866],{"x1":867,"y1":868,"x2":867,"y2":869,"stroke":91,"strokeWidth":870,"style":871},"43.0","95.5","130.5","1.5","stroke-opacity:0.3",[375,873],{"x1":867,"y1":874,"x2":867,"y2":875,"stroke":91,"strokeWidth":870,"style":871},"158.5","193.5",[375,877],{"x1":867,"y1":878,"x2":867,"y2":879,"stroke":91,"strokeWidth":870,"style":871},"221.5","256.5",[375,881],{"x1":867,"y1":882,"x2":867,"y2":883,"stroke":91,"strokeWidth":870,"style":871},"284.5","319.5",[885,886],"circle",{"cx":867,"cy":887,"r":888,"fill":195},"81.5","13",[94,890,86],{"x":867,"y":137,"fill":76,"style":817,"textAnchor":119},[885,892],{"cx":867,"cy":893,"r":888,"fill":195},"144.5",[94,895,897],{"x":867,"y":896,"fill":76,"style":817,"textAnchor":119},"149.0","2",[885,899],{"cx":867,"cy":900,"r":888,"fill":195},"207.5",[94,902,339],{"x":867,"y":903,"fill":76,"style":817,"textAnchor":119},"212.0",[885,905],{"cx":867,"cy":906,"r":888,"fill":195},"270.5",[94,908,910],{"x":867,"y":909,"fill":76,"style":817,"textAnchor":119},"275.0","4",[885,912],{"cx":867,"cy":913,"r":888,"fill":195},"333.5",[94,915,917],{"x":867,"y":916,"fill":76,"style":817,"textAnchor":119},"338.0","5",[231,919,921],{"id":920},"verification","Verification",[15,923,924,925,928],{},"Use ",[30,926,927],{},"getEventListeners(window)"," to confirm no unload listeners remain, run the DevTools bfcache test, and compare analytics completeness for mobile sessions before and after — session-end events should increase. The bfcache restoration rate in RUM should rise by roughly the share of misses previously attributed to unload.",[231,930,932],{"id":931},"worked-example-session-replay-vendor","Worked Example: Session Replay Vendor",[15,934,935,936,938,939,941,942,944],{},"A product team found that 70% of their bfcache misses were caused by an ",[30,937,32],{}," handler registered by a session-replay script. The vendor's current version supported ",[30,940,45],{},"-based flushing behind a configuration flag; enabling it removed the handler. Session-replay completeness on mobile actually improved, because the old ",[30,943,32],{}," flush had frequently not fired when users switched apps. The restoration rate rose from 22% to 71% on the affected templates.",[231,946,948],{"id":947},"common-mistakes","Common Mistakes",[236,950,951,957,973,982],{},[239,952,953,956],{},[242,954,955],{},"Moving unload code to beforeunload."," That just trades one problem for another.",[239,958,959,962,963,965,966,969,970,277],{},[242,960,961],{},"Using synchronous XHR in exit handlers."," It is deprecated, blocks navigation and is ignored by many browsers; use ",[30,964,496],{}," or ",[30,967,968],{},"fetch"," with ",[30,971,972],{},"keepalive",[239,974,975,978,979,981],{},[242,976,977],{},"Sending only on pagehide."," On mobile, ",[30,980,45],{}," is more reliable; use both if needed and de-duplicate.",[239,983,984,987],{},[242,985,986],{},"Forgetting iframes."," An unload handler in any same-page frame blocks the whole page.",[231,989,991],{"id":990},"edge-cases","Edge Cases",[15,993,994,997,998,965,1000,1002],{},[242,995,996],{},"Single-page apps."," Route changes do not fire ",[30,999,53],{},[30,1001,45],{},"; flush analytics on route changes as well as on hide.",[15,1004,1005,1008,1009,1011,1012,1014],{},[242,1006,1007],{},"Prerendered pages."," A prerendered page that is never activated fires no ",[30,1010,45],{}," to ",[30,1013,49],{}," from the user's perspective; do not count prerender-only sessions as real visits.",[15,1016,1017,1023,1024,1026],{},[242,1018,1019,969,1021,277],{},[30,1020,968],{},[30,1022,972],{}," An alternative to ",[30,1025,496],{}," that allows custom headers and methods, with a combined body-size limit across in-flight keepalive requests.",[15,1028,1029,69,1032,1034,1035,1037,1038,1040],{},[242,1030,1031],{},"Old browsers.",[30,1033,53],{}," and ",[30,1036,45],{}," are universally supported in modern browsers; no fallback to ",[30,1039,32],{}," is needed.",[231,1042,1044],{"id":1043},"migrating-a-codebase-safely","Migrating a Codebase Safely",[15,1046,1047,1048,1050,1051,1053,1054,1056,1057,1059,1060,1062,1063,1066],{},"Removing ",[30,1049,32],{}," usually touches analytics, persistence and cleanup code owned by different teams. A safe sequence: first add the replacement (",[30,1052,45],{},"\u002F",[30,1055,53],{},") alongside the existing ",[30,1058,32],{}," handler for one release, tagging events with which handler sent them, so you can confirm the new path delivers at least as much data. Then remove the ",[30,1061,32],{}," handler and keep the tag for a few weeks to watch for gaps. Finally, add the ",[30,1064,1065],{},"unload=()"," Permissions-Policy in report-only fashion where your monitoring supports it, or on a subset of pages first, to catch any third party that still depends on unload before enforcing it everywhere.",[231,1068,1070],{"id":1069},"faq","FAQ",[1072,1073,1076,1080],"details",{"className":1074},[1075],"faq-item",[1077,1078,1079],"summary",{},"Is unload being removed from browsers?",[15,1081,1082,1083,1085,1086,1088],{},"Chrome has been gradually deprecating it, with a plan to stop firing ",[30,1084,32],{}," by default and the ",[30,1087,1065],{}," Permissions-Policy available to opt out early. Treat it as obsolete and remove it now.",[1072,1090,1092,1095],{"className":1091},[1075],[1077,1093,1094],{},"Does visibilitychange fire when the user closes the tab?",[15,1096,1097,1098,1100,1101,1103],{},"Yes — the page becomes hidden before it is unloaded, so ",[30,1099,45],{}," fires. It also fires in cases where ",[30,1102,32],{}," does not, such as mobile app switching followed by a discard.",[1072,1105,1107,1110],{"className":1106},[1075],[1077,1108,1109],{},"What is the size limit for sendBeacon?",[15,1111,1112,1113,1115,1116,969,1118,1120],{},"Browsers cap queued beacon data (commonly around 64KB per page, shared across beacons). ",[30,1114,496],{}," returns false when a payload cannot be queued; fall back to ",[30,1117,968],{},[30,1119,972],{}," or trim the payload.",[1072,1122,1124,1127],{"className":1123},[1075],[1077,1125,1126],{},"Do I need both pagehide and visibilitychange?",[15,1128,1129,1130,1132,1133,1135],{},"For analytics, ",[30,1131,45],{}," alone is usually sufficient and most reliable. Use ",[30,1134,53],{}," for lifecycle-specific logic, such as pausing activity before the page enters bfcache.",[1072,1137,1139,1142],{"className":1138},[1075],[1077,1140,1141],{},"How do I find which vendor registered an unload handler?",[15,1143,1144,1145,1147],{},"In DevTools, the Event Listeners panel and ",[30,1146,248],{}," show the handler function with a link to its source file; the file's origin identifies the vendor.",[231,1149,1151],{"id":1150},"related","Related",[236,1153,1154,1161,1168],{},[239,1155,1156,1160],{},[19,1157,1159],{"href":1158},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002F","Sending web vitals with sendBeacon on visibility change"," — the same pattern for RUM beacons.",[239,1162,1163,1167],{},[19,1164,1166],{"href":1165},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Ffixing-bfcache-eligibility-blockers\u002F","Fixing bfcache eligibility blockers"," — the full list of blockers.",[239,1169,1170,1174],{},[19,1171,1173],{"href":1172},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fmodern-module-formats-esm-vs-commonjs\u002Fdeferring-non-critical-analytics-scripts-safely\u002F","Deferring non-critical analytics scripts safely"," — analytics loading that pairs with these sending patterns.",[1176,1177,1179],"script",{"type":1178},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Replace unload Handlers So Pages Can Use bfcache\",\n  \"description\": \"Why unload is both unreliable and a bfcache blocker, and how to replace every use with pagehide, visibilitychange and sendBeacon.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Send analytics on visibilitychange with sendBeacon\",\n      \"text\": \"Expected outcome: more complete data on mobile and no bfcache block.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Move cleanup to pagehide and make it reversible\",\n      \"text\": \"Expected outcome: cleanup happens without blocking restoration, and restored pages resume.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Save state continuously, not on exit\",\n      \"text\": \"Write drafts and UI state when they change (debounced) or on visibilitychange, instead of relying on an exit event.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Use beforeunload only conditionally\",\n      \"text\": \"If you need an \\\"unsaved changes\\\" prompt, add the beforeunload listener only while there are unsaved changes and remove it as soon as they are saved.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 5,\n      \"name\": \"Block unload site-wide with Permissions-Policy\",\n      \"text\": \"Once your code and vendors no longer rely on it, Permissions-Policy: unload=() prevents any frame from registering unload handlers.\"\n    }\n  ]\n}\n",[1176,1181,1182],{"type":1178},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Replace unload Handlers So Pages Can Use bfcache\",\n  \"description\": \"Why unload is both unreliable and a bfcache blocker, and how to replace every use with pagehide, visibilitychange and sendBeacon.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Freplacing-unload-handlers-for-bfcache\u002F\"\n  }\n}\n",[1176,1184,1185],{"type":1178},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Advanced Caching Strategies & CDN Architecture\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Back\u002FForward Cache (bfcache)\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Replacing unload Handlers for bfcache\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Freplacing-unload-handlers-for-bfcache\u002F\"\n    }\n  ]\n}\n",[1187,1188,1189],"style",{},"html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}",{"title":431,"searchDepth":462,"depth":462,"links":1191},[1192,1193,1194,1201,1202,1203,1204,1205,1206,1207],{"id":233,"depth":462,"text":234},{"id":280,"depth":462,"text":281},{"id":418,"depth":462,"text":419,"children":1195},[1196,1197,1198,1199,1200],{"id":423,"depth":490,"text":424},{"id":561,"depth":490,"text":562},{"id":673,"depth":490,"text":674},{"id":683,"depth":490,"text":684},{"id":780,"depth":490,"text":781},{"id":920,"depth":462,"text":921},{"id":931,"depth":462,"text":932},{"id":947,"depth":462,"text":948},{"id":990,"depth":462,"text":991},{"id":1043,"depth":462,"text":1044},{"id":1069,"depth":462,"text":1070},{"id":1150,"depth":462,"text":1151},"Why unload is both unreliable and a bfcache blocker, and how to replace every use with pagehide, visibilitychange and sendBeacon.","md",{"slug":1211,"type":1212,"breadcrumb":1213,"datePublished":1220,"dateModified":1220},"replacing-unload-handlers-for-bfcache","article",[1214,1216,1217,1218],{"name":1215,"url":1053},"Home",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1219},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Freplacing-unload-handlers-for-bfcache\u002F","2026-10-06",true,"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Freplacing-unload-handlers-for-bfcache",{"title":1224,"description":1225},"Replacing unload Handlers to Enable bfcache","unload handlers block the back\u002Fforward cache and fire unreliably on mobile. Replace them with pagehide and visibilitychange, send data with sendBeacon.","advanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Freplacing-unload-handlers-for-bfcache\u002Findex","YggAwe6S06wJps5aEErkTI3huwQ5FyL0k1QssF1R9Wg",[1229,1233],{"title":1230,"path":1231,"stem":1232},"Measuring bfcache Hit Rate in RUM","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Fmeasuring-bfcache-hit-rate-in-rum","advanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Fmeasuring-bfcache-hit-rate-in-rum\u002Findex",{"title":1234,"path":1235,"stem":1236},"Restoring State on pageshow After bfcache","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Frestoring-state-on-pageshow-after-bfcache","advanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Frestoring-state-on-pageshow-after-bfcache\u002Findex",1791308077955]