[{"data":1,"prerenderedAt":1052},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change":1043},{"id":4,"title":5,"body":6,"description":1025,"extension":1026,"meta":1027,"navigation":327,"path":1038,"seo":1039,"stem":1041,"__hash__":1042},"content\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002Findex.md","Sending Web Vitals with sendBeacon on visibilitychange",{"type":7,"value":8,"toc":1017},"minimark",[9,14,29,34,79,227,231,245,267,273,282,286,291,471,474,482,765,776,785,815,818,833,837,840,843,868,877,881,907,928,937,952,961,977,981,1002,1007,1010,1013],[10,11,13],"h1",{"id":12},"how-to-send-web-vitals-reliably-with-sendbeacon-on-visibilitychange","How to Send Web Vitals Reliably with sendBeacon on visibilitychange",[15,16,17,18,23,24,28],"p",{},"This walkthrough belongs to ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002F","RUM Beacons & Field Data Collection"," under ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",". The symptom is specific and common: your dashboard receives far fewer INP and CLS values than page views, mobile is worse than desktop, and the p75 you report looks suspiciously good. The measurements are correct; the delivery is losing them, and it is losing them non-randomly — the sessions most likely to be dropped are the frustrated ones that closed the tab.",[30,31,33],"h2",{"id":32},"rapid-diagnosis-prove-you-are-losing-beacons","Rapid Diagnosis: Prove You Are Losing Beacons",[35,36,37,45,51,66],"ul",{},[38,39,40,44],"li",{},[41,42,43],"strong",{},"Compare counts, not values."," Divide beacons received by page views from an independent source. Anything below ~95% is a delivery bug, not user behaviour.",[38,46,47,50],{},[41,48,49],{},"Split by metric."," LCP usually arrives; INP and CLS often do not. That asymmetry is the signature — they are the metrics that finalise at page hide.",[38,52,53,56,57,61,62,65],{},[41,54,55],{},"Split by platform."," If iOS and Android lose far more than desktop, you are almost certainly relying on ",[58,59,60],"code",{},"unload"," or ",[58,63,64],{},"beforeunload",", which mobile browsers frequently never fire.",[38,67,68,71,72,75,76,78],{},[41,69,70],{},"Look for bfcache disqualification."," Chrome DevTools' ",[41,73,74],{},"Application → Back\u002Fforward cache"," panel names the blocker. An ",[58,77,60],{}," listener appears there explicitly — and if it disqualifies the page from bfcache, you have also made navigation slower for real users.",[80,81,88,89,96,97,96,101,96,105,96,114,96,121,96,130,96,137,96,142,96,149,96,154,96,160,96,167,96,171,96,174,96,178,96,181,96,187,96,193,96,195,96,199,96,204,96,207,96,210,96,214,96,218,96,222,96],"svg",{"xmlns":82,"viewBox":83,"width":84,"role":85,"ariaLabel":86,"style":87},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 250","100%","img","Page lifecycle states with the events that fire on each transition, marking visibilitychange to hidden as the only reliable flush point before the page is frozen, discarded or restored from back-forward cache","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)","\n  ",[90,91],"rect",{"className":92,"x":94,"y":94,"width":84,"height":84,"fill":95},[93],"svg-canvas","0","#ffffff"," ",[98,99,100],"title",{},"The page lifecycle and the only reliable flush point",[102,103,104],"desc",{},"From active, a page passes through hidden on tab switch or navigation and can then be frozen, restored from the back-forward cache, or discarded. The hidden transition is the last moment guaranteed to fire.",[90,106],{"x":107,"y":107,"width":108,"height":109,"rx":110,"fill":111,"stroke":112,"style":113},"1","758","248","10","none","currentColor","stroke-opacity:0.18",[115,116,120],"text",{"x":117,"y":118,"fill":112,"style":119},"24","32","font-size:16px;font-weight:700","Flush here, or not at all",[90,122],{"x":123,"y":124,"width":125,"height":126,"rx":127,"fill":128,"stroke":128,"style":129},"40","80","140","52","7","#0466c8","fill-opacity:0.14;stroke-opacity:0.5",[115,131,136],{"x":132,"y":133,"fill":112,"style":134,"textAnchor":135},"110","103","font-size:12px;font-weight:700;","middle","active",[115,138,141],{"x":132,"y":139,"fill":112,"style":140,"textAnchor":135},"121","font-size:10.5px;","metrics accumulating",[143,144],"line",{"x1":145,"y1":146,"x2":147,"y2":146,"stroke":112,"style":148},"180","106","212","stroke-opacity:0.4",[150,151],"polygon",{"points":152,"fill":112,"style":153},"220,106 210,101 210,111","fill-opacity:0.55",[115,155,159],{"x":156,"y":157,"fill":112,"style":158,"textAnchor":135},"200","72","font-size:10px;fill-opacity:0.7","tab switch",[90,161],{"x":162,"y":124,"width":163,"height":126,"rx":127,"fill":164,"stroke":165,"style":166},"220","160","#ffc300","#b8860b","fill-opacity:0.28",[115,168,170],{"x":169,"y":133,"fill":112,"style":134,"textAnchor":135},"300","hidden",[115,172,173],{"x":169,"y":139,"fill":112,"style":140,"textAnchor":135},"visibilitychange fires",[143,175],{"x1":176,"y1":146,"x2":177,"y2":146,"stroke":112,"style":148},"380","412",[150,179],{"points":180,"fill":112,"style":153},"420,106 410,101 410,111",[90,182],{"x":183,"y":126,"width":184,"height":185,"rx":127,"fill":112,"stroke":112,"style":186},"420","150","42","fill-opacity:0.05;stroke-opacity:0.3",[115,188,192],{"x":189,"y":190,"fill":112,"style":191,"textAnchor":135},"495","78","font-size:11.5px;","frozen (bfcache)",[90,194],{"x":183,"y":146,"width":184,"height":185,"rx":127,"fill":112,"stroke":112,"style":186},[115,196,198],{"x":189,"y":197,"fill":112,"style":191,"textAnchor":135},"132","discarded",[143,200],{"x1":201,"y1":202,"x2":203,"y2":202,"stroke":112,"style":148},"570","73","602",[150,205],{"points":206,"fill":112,"style":153},"610,73 600,68 600,78",[90,208],{"x":209,"y":126,"width":132,"height":185,"rx":127,"fill":128,"stroke":128,"style":129},"610",[115,211,213],{"x":212,"y":190,"fill":112,"style":191,"textAnchor":135},"665","restored",[115,215,217],{"x":117,"y":145,"fill":112,"style":216},"font-size:11.5px","`unload` may never fire on mobile, and registering it disables the back\u002Fforward cache entirely.",[115,219,221],{"x":117,"y":220,"fill":112,"style":216},"204","`pagehide` is reliable too, but fires later and not on a plain tab switch.",[115,223,226],{"x":117,"y":224,"fill":112,"style":225},"228","font-size:11.5px;font-weight:700","The hidden transition is the last point guaranteed to run on every platform.",[30,228,230],{"id":229},"root-cause-analysis","Root Cause Analysis",[15,232,233,96,236,238,239,241,242,244],{},[41,234,235],{},"1. Listening on the wrong event.",[58,237,60],{}," and ",[58,240,64],{}," are legacy events that mobile browsers routinely skip when the user switches apps or the OS reclaims the tab. Worse, registering ",[58,243,60],{}," disqualifies the page from the back\u002Fforward cache, so you slow down navigation in exchange for data you do not receive.",[15,246,247,258,259,261,262,266],{},[41,248,249,250,253,254,257],{},"2. Using ",[58,251,252],{},"fetch"," without ",[58,255,256],{},"keepalive","."," A normal ",[58,260,252],{}," is cancelled when its document is destroyed. If it has not completed by the time teardown happens — which is the common case, since you are firing it ",[263,264,265],"em",{},"at"," teardown — it is aborted silently.",[15,268,269,272],{},[41,270,271],{},"3. Firing one request per metric."," Four separate beacons means four chances to be cut off, and browsers cap in-flight keepalive requests. The last metric to finalise, usually INP, is the one that loses.",[15,274,275,278,279,281],{},[41,276,277],{},"4. Double-counting in single-page apps."," The ",[58,280,170],{}," transition fires on every tab switch, not only on final departure. Without a guard, a user who switches tabs three times sends three beacons for the same page view and skews every percentile toward whatever partial values were current at each flush.",[30,283,285],{"id":284},"step-by-step-resolution","Step-by-Step Resolution",[15,287,288],{},[41,289,290],{},"1. Accumulate into a queue rather than sending immediately.",[292,293,298],"pre",{"className":294,"code":295,"language":296,"meta":297,"style":297},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","import { onLCP, onINP, onCLS, onTTFB } from 'web-vitals\u002Fattribution';\n\nconst queue = new Map();                          \u002F\u002F one entry per metric name\nconst record = (m) => queue.set(m.name, {\n  name: m.name, value: Math.round(m.value * 1000) \u002F 1000,\n  rating: m.rating, id: m.id, navType: m.navigationType\n});\n[onLCP, onINP, onCLS, onTTFB].forEach((on) => on(record));\n\u002F\u002F trade-off: a Map keyed by metric name overwrites earlier values for the same\n\u002F\u002F metric, which is what you want for a final report. If you need the interim\n\u002F\u002F series for debugging, use an array and dedupe server-side instead.\n","javascript","",[58,299,300,322,329,356,388,416,422,428,453,459,465],{"__ignoreMap":297},[301,302,304,308,312,315,319],"span",{"class":143,"line":303},1,[301,305,307],{"class":306},"sPARh","import",[301,309,311],{"class":310},"saISM"," { onLCP, onINP, onCLS, onTTFB } ",[301,313,314],{"class":306},"from",[301,316,318],{"class":317},"sZ8jY"," 'web-vitals\u002Fattribution'",[301,320,321],{"class":310},";\n",[301,323,325],{"class":143,"line":324},2,[301,326,328],{"emptyLinePlaceholder":327},true,"\n",[301,330,332,335,339,342,345,349,352],{"class":143,"line":331},3,[301,333,334],{"class":306},"const",[301,336,338],{"class":337},"sPXB4"," queue",[301,340,341],{"class":306}," =",[301,343,344],{"class":306}," new",[301,346,348],{"class":347},"smZ65"," Map",[301,350,351],{"class":310},"();                          ",[301,353,355],{"class":354},"sjfSM","\u002F\u002F one entry per metric name\n",[301,357,359,361,364,366,369,373,376,379,382,385],{"class":143,"line":358},4,[301,360,334],{"class":306},[301,362,363],{"class":347}," record",[301,365,341],{"class":306},[301,367,368],{"class":310}," (",[301,370,372],{"class":371},"sQw3B","m",[301,374,375],{"class":310},") ",[301,377,378],{"class":306},"=>",[301,380,381],{"class":310}," queue.",[301,383,384],{"class":347},"set",[301,386,387],{"class":310},"(m.name, {\n",[301,389,391,394,397,400,403,406,408,411,413],{"class":143,"line":390},5,[301,392,393],{"class":310},"  name: m.name, value: Math.",[301,395,396],{"class":347},"round",[301,398,399],{"class":310},"(m.value ",[301,401,402],{"class":306},"*",[301,404,405],{"class":337}," 1000",[301,407,375],{"class":310},[301,409,410],{"class":306},"\u002F",[301,412,405],{"class":337},[301,414,415],{"class":310},",\n",[301,417,419],{"class":143,"line":418},6,[301,420,421],{"class":310},"  rating: m.rating, id: m.id, navType: m.navigationType\n",[301,423,425],{"class":143,"line":424},7,[301,426,427],{"class":310},"});\n",[301,429,431,434,437,440,443,445,447,450],{"class":143,"line":430},8,[301,432,433],{"class":310},"[onLCP, onINP, onCLS, onTTFB].",[301,435,436],{"class":347},"forEach",[301,438,439],{"class":310},"((",[301,441,442],{"class":371},"on",[301,444,375],{"class":310},[301,446,378],{"class":306},[301,448,449],{"class":347}," on",[301,451,452],{"class":310},"(record));\n",[301,454,456],{"class":143,"line":455},9,[301,457,458],{"class":354},"\u002F\u002F trade-off: a Map keyed by metric name overwrites earlier values for the same\n",[301,460,462],{"class":143,"line":461},10,[301,463,464],{"class":354},"\u002F\u002F metric, which is what you want for a final report. If you need the interim\n",[301,466,468],{"class":143,"line":467},11,[301,469,470],{"class":354},"\u002F\u002F series for debugging, use an array and dedupe server-side instead.\n",[15,472,473],{},"Expected outcome: at most one entry per metric per page view, ready to send as a single payload.",[15,475,476],{},[41,477,478,479,257],{},"2. Flush on the hidden transition with ",[58,480,481],{},"sendBeacon",[292,483,485],{"className":294,"code":484,"language":296,"meta":297,"style":297},"function flush() {\n  if (!queue.size) return;\n  const payload = JSON.stringify({\n    v: 1, url: ROUTE_PATTERN, visitId: VISIT_ID, metrics: [...queue.values()]\n  });\n  queue.clear();                                  \u002F\u002F clear BEFORE sending\n  const blob = new Blob([payload], { type: 'application\u002Fjson' });\n  if (!navigator.sendBeacon('\u002Frum', blob)) {\n    fetch('\u002Frum', { body: payload, method: 'POST', keepalive: true }).catch(() => {});\n  }\n}\n\naddEventListener('visibilitychange', () => {\n  if (document.visibilityState === 'hidden') flush();\n});\naddEventListener('pagehide', flush);              \u002F\u002F belt and braces, idempotent\n\u002F\u002F trade-off: sendBeacon returns only whether the request was QUEUED. It cannot\n\u002F\u002F report a 500 from your collector, so client-side success is not evidence of\n\u002F\u002F delivery — monitor received volume server-side.\n",[58,486,487,498,516,537,571,576,590,613,635,670,675,680,685,704,726,731,747,753,759],{"__ignoreMap":297},[301,488,489,492,495],{"class":143,"line":303},[301,490,491],{"class":306},"function",[301,493,494],{"class":347}," flush",[301,496,497],{"class":310},"() {\n",[301,499,500,503,505,508,511,514],{"class":143,"line":324},[301,501,502],{"class":306},"  if",[301,504,368],{"class":310},[301,506,507],{"class":306},"!",[301,509,510],{"class":310},"queue.size) ",[301,512,513],{"class":306},"return",[301,515,321],{"class":310},[301,517,518,521,524,526,529,531,534],{"class":143,"line":331},[301,519,520],{"class":306},"  const",[301,522,523],{"class":337}," payload",[301,525,341],{"class":306},[301,527,528],{"class":337}," JSON",[301,530,257],{"class":310},[301,532,533],{"class":347},"stringify",[301,535,536],{"class":310},"({\n",[301,538,539,542,544,547,550,553,556,559,562,565,568],{"class":143,"line":358},[301,540,541],{"class":310},"    v: ",[301,543,107],{"class":337},[301,545,546],{"class":310},", url: ",[301,548,549],{"class":337},"ROUTE_PATTERN",[301,551,552],{"class":310},", visitId: ",[301,554,555],{"class":337},"VISIT_ID",[301,557,558],{"class":310},", metrics: [",[301,560,561],{"class":306},"...",[301,563,564],{"class":310},"queue.",[301,566,567],{"class":347},"values",[301,569,570],{"class":310},"()]\n",[301,572,573],{"class":143,"line":390},[301,574,575],{"class":310},"  });\n",[301,577,578,581,584,587],{"class":143,"line":418},[301,579,580],{"class":310},"  queue.",[301,582,583],{"class":347},"clear",[301,585,586],{"class":310},"();                                  ",[301,588,589],{"class":354},"\u002F\u002F clear BEFORE sending\n",[301,591,592,594,597,599,601,604,607,610],{"class":143,"line":424},[301,593,520],{"class":306},[301,595,596],{"class":337}," blob",[301,598,341],{"class":306},[301,600,344],{"class":306},[301,602,603],{"class":347}," Blob",[301,605,606],{"class":310},"([payload], { type: ",[301,608,609],{"class":317},"'application\u002Fjson'",[301,611,612],{"class":310}," });\n",[301,614,615,617,619,621,624,626,629,632],{"class":143,"line":430},[301,616,502],{"class":306},[301,618,368],{"class":310},[301,620,507],{"class":306},[301,622,623],{"class":310},"navigator.",[301,625,481],{"class":347},[301,627,628],{"class":310},"(",[301,630,631],{"class":317},"'\u002Frum'",[301,633,634],{"class":310},", blob)) {\n",[301,636,637,640,642,644,647,650,653,656,659,662,665,667],{"class":143,"line":455},[301,638,639],{"class":347},"    fetch",[301,641,628],{"class":310},[301,643,631],{"class":317},[301,645,646],{"class":310},", { body: payload, method: ",[301,648,649],{"class":317},"'POST'",[301,651,652],{"class":310},", keepalive: ",[301,654,655],{"class":337},"true",[301,657,658],{"class":310}," }).",[301,660,661],{"class":347},"catch",[301,663,664],{"class":310},"(() ",[301,666,378],{"class":306},[301,668,669],{"class":310}," {});\n",[301,671,672],{"class":143,"line":461},[301,673,674],{"class":310},"  }\n",[301,676,677],{"class":143,"line":467},[301,678,679],{"class":310},"}\n",[301,681,683],{"class":143,"line":682},12,[301,684,328],{"emptyLinePlaceholder":327},[301,686,688,691,693,696,699,701],{"class":143,"line":687},13,[301,689,690],{"class":347},"addEventListener",[301,692,628],{"class":310},[301,694,695],{"class":317},"'visibilitychange'",[301,697,698],{"class":310},", () ",[301,700,378],{"class":306},[301,702,703],{"class":310}," {\n",[301,705,707,709,712,715,718,720,723],{"class":143,"line":706},14,[301,708,502],{"class":306},[301,710,711],{"class":310}," (document.visibilityState ",[301,713,714],{"class":306},"===",[301,716,717],{"class":317}," 'hidden'",[301,719,375],{"class":310},[301,721,722],{"class":347},"flush",[301,724,725],{"class":310},"();\n",[301,727,729],{"class":143,"line":728},15,[301,730,427],{"class":310},[301,732,734,736,738,741,744],{"class":143,"line":733},16,[301,735,690],{"class":347},[301,737,628],{"class":310},[301,739,740],{"class":317},"'pagehide'",[301,742,743],{"class":310},", flush);              ",[301,745,746],{"class":354},"\u002F\u002F belt and braces, idempotent\n",[301,748,750],{"class":143,"line":749},17,[301,751,752],{"class":354},"\u002F\u002F trade-off: sendBeacon returns only whether the request was QUEUED. It cannot\n",[301,754,756],{"class":143,"line":755},18,[301,757,758],{"class":354},"\u002F\u002F report a 500 from your collector, so client-side success is not evidence of\n",[301,760,762],{"class":143,"line":761},19,[301,763,764],{"class":354},"\u002F\u002F delivery — monitor received volume server-side.\n",[15,766,767,768,771,772,775],{},"Expected outcome: delivery rates typically move from ",[58,769,770],{},"60–80%"," to above ",[58,773,774],{},"98%",", with the largest gain on mobile.",[15,777,778,781,782,784],{},[41,779,780],{},"3. Make repeat flushes safe."," Clearing the queue before sending means a second ",[58,783,170],{}," transition sends nothing unless new metrics arrived. If your collector needs to merge partial reports, key them on the visit ID so late arrivals update the same row rather than creating a new one.",[292,786,788],{"className":294,"code":787,"language":296,"meta":297,"style":297},"\u002F\u002F Collector side: upsert on (visitId, metricName) so a second flush that\n\u002F\u002F carries a larger INP replaces the earlier value instead of adding a row.\n\u002F\u002F trade-off: upserts cost more than appends at high volume. If your store\n\u002F\u002F prefers appends, write append-only and take the max per (visitId, metric)\n\u002F\u002F at query time instead.\n",[58,789,790,795,800,805,810],{"__ignoreMap":297},[301,791,792],{"class":143,"line":303},[301,793,794],{"class":354},"\u002F\u002F Collector side: upsert on (visitId, metricName) so a second flush that\n",[301,796,797],{"class":143,"line":324},[301,798,799],{"class":354},"\u002F\u002F carries a larger INP replaces the earlier value instead of adding a row.\n",[301,801,802],{"class":143,"line":331},[301,803,804],{"class":354},"\u002F\u002F trade-off: upserts cost more than appends at high volume. If your store\n",[301,806,807],{"class":143,"line":358},[301,808,809],{"class":354},"\u002F\u002F prefers appends, write append-only and take the max per (visitId, metric)\n",[301,811,812],{"class":143,"line":390},[301,813,814],{"class":354},"\u002F\u002F at query time instead.\n",[15,816,817],{},"Expected outcome: tab switching no longer inflates your page-view count or distorts percentiles.",[15,819,820,823,824,826,827,829,830,832],{},[41,821,822],{},"4. Keep the payload small and the endpoint dumb."," Stay well under the ~64KB ",[58,825,481],{}," limit, and have the collector return ",[58,828,220],{}," immediately, writing to a queue rather than a database on the request path. A collector that takes 300ms to respond does not slow the user down — ",[58,831,481],{}," is fire-and-forget — but it will fall over under real traffic.",[30,834,836],{"id":835},"verification","Verification",[15,838,839],{},"Instrument the check rather than trusting a manual test. Compare beacons received against page views over a full day, split by platform, and confirm the ratio is above 95% everywhere. Then confirm the per-metric completeness: of the visits that reported LCP, what fraction also reported CLS? A widening gap means the flush is still being missed.",[15,841,842],{},"Manually, the reliable reproduction is a tab switch:",[292,844,846],{"className":294,"code":845,"language":296,"meta":297,"style":297},"\u002F\u002F In DevTools, with the Network panel filtered to your collector path:\n\u002F\u002F 1. Load the page, interact with it once so INP has a value.\n\u002F\u002F 2. Switch to another tab. A single POST to \u002Frum should appear immediately.\n\u002F\u002F 3. Switch back and away again — no second beacon unless new metrics arrived.\n",[58,847,848,853,858,863],{"__ignoreMap":297},[301,849,850],{"class":143,"line":303},[301,851,852],{"class":354},"\u002F\u002F In DevTools, with the Network panel filtered to your collector path:\n",[301,854,855],{"class":143,"line":324},[301,856,857],{"class":354},"\u002F\u002F 1. Load the page, interact with it once so INP has a value.\n",[301,859,860],{"class":143,"line":331},[301,861,862],{"class":354},"\u002F\u002F 2. Switch to another tab. A single POST to \u002Frum should appear immediately.\n",[301,864,865],{"class":143,"line":358},[301,866,867],{"class":354},"\u002F\u002F 3. Switch back and away again — no second beacon unless new metrics arrived.\n",[15,869,870,871,873,874,876],{},"Finally, confirm you have not broken the back\u002Fforward cache: open ",[41,872,74],{},", run the test, and check that the page is eligible. If a legacy ",[58,875,60],{}," listener still exists anywhere in your bundle or in a third-party tag, it will be named there — and removing it is both a data fix and a navigation-speed fix.",[30,878,880],{"id":879},"faq","FAQ",[882,883,886,890],"details",{"className":884},[885],"faq-item",[887,888,889],"summary",{},"Should I use pagehide or visibilitychange?",[15,891,892,893,896,897,900,901,903,904,906],{},"Use ",[58,894,895],{},"visibilitychange"," as the primary trigger and ",[58,898,899],{},"pagehide"," as a secondary one. The ",[58,902,170],{}," transition fires in every case that matters, including app switching on mobile where ",[58,905,899],{}," may come much later or not at all. Registering both is safe as long as the flush is idempotent — clear the queue before sending and a second call is a no-op.",[882,908,910,913],{"className":909},[885],[887,911,912],{},"What happens to metrics after a back\u002Fforward cache restore?",[15,914,915,916,919,920,923,924,927],{},"The page resumes with its existing state, and ",[58,917,918],{},"web-vitals"," reports a fresh set of values with ",[58,921,922],{},"navigationType"," set to ",[58,925,926],{},"back-forward-cache",". Keep those rows but analyse them separately: a restore has a near-zero LCP that will drag your cold-load p75 downward and hide regressions if you mix the two populations.",[882,929,931,934],{"className":930},[885],[887,932,933],{},"Is sendBeacon blocked by ad blockers or consent tooling?",[15,935,936],{},"The API itself is not, but the destination can be. A collector on a path containing \"analytics\" or on a known third-party domain is a common block target, which shows up as a uniform loss across a subset of users. Host the collector on your own origin under a neutral path, and monitor the received-to-pageview ratio by country and browser — a sharp drop in one segment usually means blocking, not a code bug.",[882,938,940,943],{"className":939},[885],[887,941,942],{},"How large can a beacon payload be?",[15,944,945,947,948,951],{},[58,946,481],{}," is subject to a browser-enforced quota — commonly around 64KB across all queued beacons — and returns ",[58,949,950],{},"false"," when the payload is rejected. A batch of four metrics with attribution is well under a kilobyte, so the limit only becomes relevant if you attach large diagnostic blobs. Keep the payload to scalar fields and short selectors, and check the return value so a rejection falls through to the keepalive fetch instead of vanishing.",[882,953,955,958],{"className":954},[885],[887,956,957],{},"Should the collector be on a subdomain or a path?",[15,959,960],{},"A path on the main origin, if you can. It reuses the existing connection, avoids a DNS lookup and TLS handshake at the moment the page is being torn down, and is far less likely to be blocked than a recognisable third-party endpoint. A subdomain still costs a fresh connection unless the browser can coalesce it, which is not guaranteed. Reserve a separate host for cases where the collector genuinely cannot share infrastructure with the site.",[882,962,964,967],{"className":963},[885],[887,965,966],{},"Do I need to handle the page freeze and resume events?",[15,968,969,970,972,973,976],{},"For most sites, no — the ",[58,971,170],{}," transition already fires before a freeze, so your data is out. It becomes relevant if you keep long-lived state in memory that should be flushed differently on resume, or if you periodically flush during long visible sessions and want to avoid a flush while frozen. If you add a periodic flush, gate it on ",[58,974,975],{},"document.visibilityState === 'visible'"," so it cannot fire in a background tab.",[30,978,980],{"id":979},"related","Related",[35,982,983,988,995],{},[38,984,985,987],{},[19,986,22],{"href":21}," — the full pipeline this delivery step plugs into.",[38,989,990,994],{},[19,991,993],{"href":992},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fattributing-inp-to-a-component-with-the-attribution-build\u002F","Attributing INP to a component"," — what to put in the payload so the number is actionable.",[38,996,997,1001],{},[19,998,1000],{"href":999},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fcrux-vs-your-own-rum-why-the-numbers-differ\u002F","CrUX vs your own RUM"," — delivery loss is one of the four reasons two correct numbers disagree.",[1003,1004,1006],"script",{"type":1005},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Deliver Web Vitals beacons reliably\",\n  \"description\": \"Batch Core Web Vitals into a queue and flush it once with navigator.sendBeacon on the visibilitychange hidden transition, avoiding unload, double-counting and bfcache disqualification.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"position\": 1, \"name\": \"Queue the metrics\", \"text\": \"Collect one entry per metric name instead of sending each value as it finalises.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 2, \"name\": \"Flush on hidden\", \"text\": \"Send the batch with navigator.sendBeacon when document.visibilityState becomes hidden, falling back to keepalive fetch.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 3, \"name\": \"Make repeat flushes safe\", \"text\": \"Clear the queue before sending and upsert on visit ID so tab switching cannot double-count a page view.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 4, \"name\": \"Verify delivery\", \"text\": \"Compare beacons received to page views by platform and confirm the page remains eligible for the back\u002Fforward cache.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002F#verification\" }\n  ]\n}\n",[1003,1008,1009],{"type":1005},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Send Web Vitals Reliably with sendBeacon on visibilitychange\",\n  \"description\": \"Why INP and CLS beacons go missing on mobile and how batching plus a sendBeacon flush on the hidden transition fixes delivery without breaking the back\u002Fforward cache.\",\n  \"datePublished\": \"2026-08-01\",\n  \"dateModified\": \"2026-08-01\",\n  \"author\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"publisher\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"mainEntityOfPage\": { \"@type\": \"WebPage\", \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002F\" }\n}\n",[1003,1011,1012],{"type":1005},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    { \"@type\": \"ListItem\", \"position\": 1, \"name\": \"Home\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 2, \"name\": \"Core Web Vitals & Measurement\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"RUM Beacons & Field Data Collection\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Sending Web Vitals with sendBeacon on visibilitychange\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002F\" }\n  ]\n}\n",[1014,1015,1016],"style",{},"html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}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 .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}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);}",{"title":297,"searchDepth":324,"depth":324,"links":1018},[1019,1020,1021,1022,1023,1024],{"id":32,"depth":324,"text":33},{"id":229,"depth":324,"text":230},{"id":284,"depth":324,"text":285},{"id":835,"depth":324,"text":836},{"id":879,"depth":324,"text":880},{"id":979,"depth":324,"text":980},"The delivery half of RUM: flush batched metrics on the hidden transition with sendBeacon so the slowest sessions are not the ones you silently drop.","md",{"slug":1028,"type":1029,"breadcrumb":1030,"datePublished":1037,"dateModified":1037},"sending-web-vitals-with-sendbeacon-on-visibility-change","article",[1031,1033,1034,1035],{"name":1032,"url":410},"Home",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1036},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002F","2026-08-01","\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change",{"title":5,"description":1040},"Stop losing INP and CLS beacons on unload: why visibilitychange beats unload, how sendBeacon survives teardown, and how to avoid double-counting in a SPA.","core-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002Findex","lUMaQtTC-sHErE09L3EdWUKvsKeBOf15eyPZQxZ19fM",[1044,1048],{"title":1045,"path":1046,"stem":1047,"children":-1},"CrUX vs Your Own RUM","\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fcrux-vs-your-own-rum-why-the-numbers-differ","core-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fcrux-vs-your-own-rum-why-the-numbers-differ\u002Findex",{"title":1049,"path":1050,"stem":1051,"children":-1},"Understanding Core Web Vitals Thresholds","\u002Fcore-web-vitals-measurement\u002Funderstanding-core-web-vitals-thresholds","core-web-vitals-measurement\u002Funderstanding-core-web-vitals-thresholds\u002Findex",1785622406642]