[{"data":1,"prerenderedAt":1403},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa":1395},{"id":4,"title":5,"body":6,"description":1376,"extension":1377,"meta":1378,"navigation":548,"path":1390,"seo":1391,"stem":1393,"__hash__":1394},"content\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa\u002Findex.md","Attributing INP to the Right Route in an SPA",{"type":7,"value":8,"toc":1361},"minimark",[9,14,34,37,150,155,203,207,213,219,225,236,240,245,493,500,503,507,676,679,795,799,1111,1114,1118,1132,1135,1223,1227,1234,1238,1244,1248,1262,1275,1285,1291,1295,1307,1319,1323,1346,1351,1354,1357],[10,11,13],"h1",{"id":12},"how-to-attribute-inp-to-the-route-that-actually-caused-it","How to Attribute INP to the Route That Actually Caused It",[15,16,17,18,23,24,28,29,33],"p",{},"This guide belongs to ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002F","Soft Navigations & SPA Metrics"," within ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",". The scenario is common and expensive: Search Console flags your category pages for INP above ",[30,31,32],"code",{},"200ms",", a sprint goes into optimising them, and the field number does not move — because the slow interaction was on the checkout view that users reach by in-app navigation, and every metric from that session was filed under the landing URL.",[15,35,36],{},"INP is the worst interaction (ignoring one outlier per 50) over the whole document lifetime. In a single-page app the document lifetime spans every route the user visits, so the INP value is effectively \"the slowest interaction anywhere in the session\", attributed to wherever the session began. To fix the right code you need to know where the interaction happened.",[15,38,39],{},[40,41,47,48,47,55,47,59,47,62,47,71,47,77,47,86,47,92,47,97,47,102,47,105,47,108,47,111,47,114,47,117,47,120,47,125,47,129,47,131,47,135,47,137,47,140,47,142,47,145,47,147,47],"svg",{"viewBox":42,"width":43,"role":44,"ariaLabel":45,"style":46},"0 0 760 228","100%","img","Comparison of how a session's INP is attributed by default to the landing URL versus by route-tagged RUM to the view where the slow interaction occurred.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[49,50],"rect",{"className":51,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],"svg-canvas","0","#ffffff",[56,57,58],"title",{},"Same session, two attributions",[60,61,45],"desc",{},[49,63],{"x":64,"y":64,"width":65,"height":66,"rx":67,"fill":68,"stroke":69,"style":70},"1","758","226","10","none","currentColor","stroke-opacity:0.18",[72,73,58],"text",{"x":74,"y":75,"fill":69,"style":76},"28.0","34.0","font-size:16px;font-weight:700",[49,78],{"x":74,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"56.0","340.0","150.0","6","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[72,87,91],{"x":88,"y":89,"fill":69,"style":90},"42.0","82.0","font-size:14px;font-weight:700","Default (CrUX, plain RUM)",[72,93,96],{"x":88,"y":94,"fill":69,"style":95},"108.0","font-size:12.5px;font-weight:700","•",[72,98,101],{"x":79,"y":94,"fill":69,"style":99,"textAnchor":100},"font-size:12.5px","start","Session lands on \u002Fshoes",[72,103,96],{"x":88,"y":104,"fill":69,"style":95},"132.0",[72,106,107],{"x":79,"y":104,"fill":69,"style":99,"textAnchor":100},"Slow click happens on \u002Fcheckout (340ms)",[72,109,96],{"x":88,"y":110,"fill":69,"style":95},"156.0",[72,112,113],{"x":79,"y":110,"fill":69,"style":99,"textAnchor":100},"INP 340ms charged to \u002Fshoes",[72,115,96],{"x":88,"y":116,"fill":69,"style":95},"180.0",[72,118,119],{"x":79,"y":116,"fill":69,"style":99,"textAnchor":100},"\u002Fcheckout has no INP data at all",[49,121],{"x":122,"y":79,"width":80,"height":81,"rx":82,"fill":123,"stroke":123,"style":124},"392.0","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[72,126,128],{"x":127,"y":89,"fill":69,"style":90},"406.0","Route-tagged RUM",[72,130,96],{"x":127,"y":94,"fill":69,"style":95},[72,132,134],{"x":133,"y":94,"fill":69,"style":99,"textAnchor":100},"420.0","Each interaction tagged with active route",[72,136,96],{"x":127,"y":104,"fill":69,"style":95},[72,138,139],{"x":133,"y":104,"fill":69,"style":99,"textAnchor":100},"Worst interaction found on \u002Fcheckout",[72,141,96],{"x":127,"y":110,"fill":69,"style":95},[72,143,144],{"x":133,"y":110,"fill":69,"style":99,"textAnchor":100},"INP 340ms charged to \u002Fcheckout",[72,146,96],{"x":127,"y":116,"fill":69,"style":95},[72,148,149],{"x":133,"y":116,"fill":69,"style":99,"textAnchor":100},"\u002Fshoes shows its own INP of 90ms",[151,152,154],"h2",{"id":153},"rapid-diagnosis","Rapid Diagnosis",[156,157,158,166,179,197],"ul",{},[159,160,161,165],"li",{},[162,163,164],"strong",{},"Compare INP by landing page with INP by session length."," If INP worsens steadily with pages-per-session, interactions on deeper routes are driving it.",[159,167,168,171,172,175,176,178],{},[162,169,170],{},"Reproduce the landing route alone."," Load ",[30,173,174],{},"\u002Fshoes",", interact only there, and measure. If it is comfortably under ",[30,177,32],{}," in a throttled lab run, its field INP is inherited.",[159,180,181,184,185,188,189,192,193,196],{},[162,182,183],{},"Look at the interaction target."," The ",[30,186,187],{},"web-vitals"," attribution build reports ",[30,190,191],{},"interactionTarget"," as a CSS selector. A selector like ",[30,194,195],{},"#place-order"," on a category page's INP is a giveaway.",[159,198,199,202],{},[162,200,201],{},"Check route-level traffic."," Routes reached only by soft navigation will have no rows in any document-URL report — absence of data is itself a clue.",[151,204,206],{"id":205},"root-cause-analysis","Root Cause Analysis",[15,208,209,212],{},[162,210,211],{},"1. The metric is document-scoped by definition."," Event Timing entries carry no route; the browser does not know your router exists. Any attribution to a route must come from your code.",[15,214,215,218],{},[162,216,217],{},"2. The \"worst interaction\" is a maximum across routes."," One slow route contaminates the value of every session that passes through it, regardless of where those sessions began.",[15,220,221,224],{},[162,222,223],{},"3. The interaction target is ambiguous across routes."," Shared components — a header search box, a cart drawer — appear on every route. The selector alone does not tell you which view's state made the interaction slow.",[15,226,227,230,231,235],{},[162,228,229],{},"4. Late interactions are reported late."," The final INP is only known when the page is hidden. If you capture the route at report time rather than at interaction time, you record the ",[232,233,234],"em",{},"last"," route visited, not the one where the slow interaction occurred.",[151,237,239],{"id":238},"step-by-step-resolution","Step-by-Step Resolution",[241,242,244],"h3",{"id":243},"_1-keep-a-timestamped-route-history","1. Keep a timestamped route history",[246,247,252],"pre",{"className":248,"code":249,"language":250,"meta":251,"style":251},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F route-history.js\nexport const history = [{ route: location.pathname, start: 0 }];\nexport function pushRoute(route) {\n  history.push({ route, start: performance.now() });\n  if (history.length > 50) history.splice(1, 1);   \u002F\u002F keep the landing entry\n}\nexport function routeAt(time) {\n  for (let i = history.length - 1; i >= 0; i--) if (history[i].start \u003C= time) return history[i].route;\n  return history[0].route;\n}\n\u002F\u002F trade-off: storing route templates (\"\u002Fproduct\u002F:id\") rather than concrete paths\n\u002F\u002F keeps cardinality low but loses which product was slow. Store the template in\n\u002F\u002F RUM and the concrete path only in sampled debug sessions.\n","javascript","",[30,253,254,263,289,311,329,368,374,391,456,470,475,481,487],{"__ignoreMap":251},[255,256,259],"span",{"class":257,"line":258},"line",1,[255,260,262],{"class":261},"sjfSM","\u002F\u002F route-history.js\n",[255,264,266,270,273,277,280,284,286],{"class":257,"line":265},2,[255,267,269],{"class":268},"sPARh","export",[255,271,272],{"class":268}," const",[255,274,276],{"class":275},"sPXB4"," history",[255,278,279],{"class":268}," =",[255,281,283],{"class":282},"saISM"," [{ route: location.pathname, start: ",[255,285,53],{"class":275},[255,287,288],{"class":282}," }];\n",[255,290,292,294,297,301,304,308],{"class":257,"line":291},3,[255,293,269],{"class":268},[255,295,296],{"class":268}," function",[255,298,300],{"class":299},"smZ65"," pushRoute",[255,302,303],{"class":282},"(",[255,305,307],{"class":306},"sQw3B","route",[255,309,310],{"class":282},") {\n",[255,312,314,317,320,323,326],{"class":257,"line":313},4,[255,315,316],{"class":282},"  history.",[255,318,319],{"class":299},"push",[255,321,322],{"class":282},"({ route, start: performance.",[255,324,325],{"class":299},"now",[255,327,328],{"class":282},"() });\n",[255,330,332,335,338,341,344,347,350,353,355,357,360,362,365],{"class":257,"line":331},5,[255,333,334],{"class":268},"  if",[255,336,337],{"class":282}," (history.",[255,339,340],{"class":275},"length",[255,342,343],{"class":268}," >",[255,345,346],{"class":275}," 50",[255,348,349],{"class":282},") history.",[255,351,352],{"class":299},"splice",[255,354,303],{"class":282},[255,356,64],{"class":275},[255,358,359],{"class":282},", ",[255,361,64],{"class":275},[255,363,364],{"class":282},");   ",[255,366,367],{"class":261},"\u002F\u002F keep the landing entry\n",[255,369,371],{"class":257,"line":370},6,[255,372,373],{"class":282},"}\n",[255,375,377,379,381,384,386,389],{"class":257,"line":376},7,[255,378,269],{"class":268},[255,380,296],{"class":268},[255,382,383],{"class":299}," routeAt",[255,385,303],{"class":282},[255,387,388],{"class":306},"time",[255,390,310],{"class":282},[255,392,394,397,400,403,406,409,412,414,417,420,423,426,429,432,435,438,441,444,447,450,453],{"class":257,"line":393},8,[255,395,396],{"class":268},"  for",[255,398,399],{"class":282}," (",[255,401,402],{"class":268},"let",[255,404,405],{"class":282}," i ",[255,407,408],{"class":268},"=",[255,410,411],{"class":282}," history.",[255,413,340],{"class":275},[255,415,416],{"class":268}," -",[255,418,419],{"class":275}," 1",[255,421,422],{"class":282},"; i ",[255,424,425],{"class":268},">=",[255,427,428],{"class":275}," 0",[255,430,431],{"class":282},"; i",[255,433,434],{"class":268},"--",[255,436,437],{"class":282},") ",[255,439,440],{"class":268},"if",[255,442,443],{"class":282}," (history[i].start ",[255,445,446],{"class":268},"\u003C=",[255,448,449],{"class":282}," time) ",[255,451,452],{"class":268},"return",[255,454,455],{"class":282}," history[i].route;\n",[255,457,459,462,465,467],{"class":257,"line":458},9,[255,460,461],{"class":268},"  return",[255,463,464],{"class":282}," history[",[255,466,53],{"class":275},[255,468,469],{"class":282},"].route;\n",[255,471,473],{"class":257,"line":472},10,[255,474,373],{"class":282},[255,476,478],{"class":257,"line":477},11,[255,479,480],{"class":261},"\u002F\u002F trade-off: storing route templates (\"\u002Fproduct\u002F:id\") rather than concrete paths\n",[255,482,484],{"class":257,"line":483},12,[255,485,486],{"class":261},"\u002F\u002F keeps cardinality low but loses which product was slow. Store the template in\n",[255,488,490],{"class":257,"line":489},13,[255,491,492],{"class":261},"\u002F\u002F RUM and the concrete path only in sampled debug sessions.\n",[15,494,495,496,499],{},"Call ",[30,497,498],{},"pushRoute(to.matched.at(-1).path)"," (Vue Router) or the equivalent template in your router's after-navigation hook.",[15,501,502],{},"Expected outcome: any timestamp on the page can be mapped back to the route that was on screen.",[241,504,506],{"id":505},"_2-tag-inp-with-the-route-at-the-interactions-start-time","2. Tag INP with the route at the interaction's start time",[246,508,510],{"className":248,"code":509,"language":250,"meta":251,"style":251},"import { onINP } from 'web-vitals\u002Fattribution';\nimport { routeAt, history } from '.\u002Froute-history.js';\n\nonINP(({ value, attribution }) => {\n  report({\n    metric: 'INP',\n    value: Math.round(value),\n    route: routeAt(attribution.interactionTime),     \u002F\u002F where it happened\n    landing: history[0].route,                       \u002F\u002F where the session began\n    target: attribution.interactionTarget,\n    phase: { input: attribution.inputDelay, processing: attribution.processingDuration,\n             presentation: attribution.presentationDelay },\n  });\n});\n\u002F\u002F trade-off: this still reports ONE INP per session — the worst. A route that\n\u002F\u002F is consistently 180ms will never surface if another route is worse. Step 3\n\u002F\u002F adds per-route maxima to fix that.\n",[30,511,512,530,544,550,575,583,594,605,619,632,637,642,647,652,658,664,670],{"__ignoreMap":251},[255,513,514,517,520,523,527],{"class":257,"line":258},[255,515,516],{"class":268},"import",[255,518,519],{"class":282}," { onINP } ",[255,521,522],{"class":268},"from",[255,524,526],{"class":525},"sZ8jY"," 'web-vitals\u002Fattribution'",[255,528,529],{"class":282},";\n",[255,531,532,534,537,539,542],{"class":257,"line":265},[255,533,516],{"class":268},[255,535,536],{"class":282}," { routeAt, history } ",[255,538,522],{"class":268},[255,540,541],{"class":525}," '.\u002Froute-history.js'",[255,543,529],{"class":282},[255,545,546],{"class":257,"line":291},[255,547,549],{"emptyLinePlaceholder":548},true,"\n",[255,551,552,555,558,561,563,566,569,572],{"class":257,"line":313},[255,553,554],{"class":299},"onINP",[255,556,557],{"class":282},"(({ ",[255,559,560],{"class":306},"value",[255,562,359],{"class":282},[255,564,565],{"class":306},"attribution",[255,567,568],{"class":282}," }) ",[255,570,571],{"class":268},"=>",[255,573,574],{"class":282}," {\n",[255,576,577,580],{"class":257,"line":331},[255,578,579],{"class":299},"  report",[255,581,582],{"class":282},"({\n",[255,584,585,588,591],{"class":257,"line":370},[255,586,587],{"class":282},"    metric: ",[255,589,590],{"class":525},"'INP'",[255,592,593],{"class":282},",\n",[255,595,596,599,602],{"class":257,"line":376},[255,597,598],{"class":282},"    value: Math.",[255,600,601],{"class":299},"round",[255,603,604],{"class":282},"(value),\n",[255,606,607,610,613,616],{"class":257,"line":393},[255,608,609],{"class":282},"    route: ",[255,611,612],{"class":299},"routeAt",[255,614,615],{"class":282},"(attribution.interactionTime),     ",[255,617,618],{"class":261},"\u002F\u002F where it happened\n",[255,620,621,624,626,629],{"class":257,"line":458},[255,622,623],{"class":282},"    landing: history[",[255,625,53],{"class":275},[255,627,628],{"class":282},"].route,                       ",[255,630,631],{"class":261},"\u002F\u002F where the session began\n",[255,633,634],{"class":257,"line":472},[255,635,636],{"class":282},"    target: attribution.interactionTarget,\n",[255,638,639],{"class":257,"line":477},[255,640,641],{"class":282},"    phase: { input: attribution.inputDelay, processing: attribution.processingDuration,\n",[255,643,644],{"class":257,"line":483},[255,645,646],{"class":282},"             presentation: attribution.presentationDelay },\n",[255,648,649],{"class":257,"line":489},[255,650,651],{"class":282},"  });\n",[255,653,655],{"class":257,"line":654},14,[255,656,657],{"class":282},"});\n",[255,659,661],{"class":257,"line":660},15,[255,662,663],{"class":261},"\u002F\u002F trade-off: this still reports ONE INP per session — the worst. A route that\n",[255,665,667],{"class":257,"line":666},16,[255,668,669],{"class":261},"\u002F\u002F is consistently 180ms will never surface if another route is worse. Step 3\n",[255,671,673],{"class":257,"line":672},17,[255,674,675],{"class":261},"\u002F\u002F adds per-route maxima to fix that.\n",[15,677,678],{},"Expected outcome: the session's INP is filed under the route where the worst interaction occurred.",[15,680,681],{},[40,682,47,685,47,688,47,691,47,693,47,710,47,713,47,715,47,719,47,726,47,731,47,735,47,738,47,742,47,745,47,748,47,752,47,755,47,758,47,762,47,765,47,768,47,772,47,775,47,783,47,787,47,791,47],{"viewBox":683,"width":43,"role":44,"ariaLabel":684,"style":46},"0 0 760 149","Flow from router navigation events to a timestamped route history, then mapping each interaction's start time to a route before reporting.",[49,686],{"className":687,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,689,690],{},"Route tagging pipeline",[60,692,684],{},[694,695,696],"defs",{},[697,698,705],"marker",{"id":699,"viewBox":700,"refX":701,"refY":702,"markerWidth":703,"markerHeight":703,"orient":704},"faeb095f60","0 0 10 10","9","5","7","auto-start-reverse",[706,707],"path",{"d":708,"fill":69,"style":709},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[49,711],{"x":64,"y":64,"width":65,"height":712,"rx":67,"fill":68,"stroke":69,"style":70},"147",[72,714,690],{"x":74,"y":75,"fill":69,"style":76},[49,716],{"x":74,"y":79,"width":717,"height":718,"rx":82,"fill":123,"stroke":123,"style":124},"108.8","71.0",[72,720,725],{"x":721,"y":722,"fill":69,"style":723,"textAnchor":724},"82.4","78.0","font-size:13px;font-weight:700","middle","Router hook",[72,727,730],{"x":721,"y":728,"fill":69,"style":729,"textAnchor":724},"96.0","font-size:12px","push route",[72,732,734],{"x":721,"y":733,"fill":69,"style":729,"textAnchor":724},"112.0","template",[49,736],{"x":737,"y":79,"width":717,"height":718,"rx":82,"fill":123,"stroke":123,"style":124},"176.8",[72,739,741],{"x":740,"y":722,"fill":69,"style":723,"textAnchor":724},"231.2","Route history",[72,743,744],{"x":740,"y":728,"fill":69,"style":729,"textAnchor":724},"route + start time",[49,746],{"x":747,"y":79,"width":717,"height":718,"rx":82,"fill":123,"stroke":123,"style":124},"325.6",[72,749,751],{"x":750,"y":722,"fill":69,"style":723,"textAnchor":724},"380.0","Interaction",[72,753,754],{"x":750,"y":728,"fill":69,"style":729,"textAnchor":724},"interactionTime",[49,756],{"x":757,"y":79,"width":717,"height":718,"rx":82,"fill":83,"stroke":84,"style":85},"474.4",[72,759,761],{"x":760,"y":722,"fill":69,"style":723,"textAnchor":724},"528.8","routeAt(t)",[72,763,764],{"x":760,"y":728,"fill":69,"style":729,"textAnchor":724},"lookup",[49,766],{"x":767,"y":79,"width":717,"height":718,"rx":82,"fill":123,"stroke":123,"style":124},"623.2",[72,769,771],{"x":770,"y":722,"fill":69,"style":723,"textAnchor":724},"677.6","Beacon",[72,773,774],{"x":770,"y":728,"fill":69,"style":729,"textAnchor":724},"INP + route",[257,776],{"x1":777,"y1":778,"x2":779,"y2":778,"stroke":69,"strokeWidth":780,"style":781,"markerEnd":782},"136.8","91.5","174.8","1.5","stroke-opacity:0.6","url(#faeb095f60)",[257,784],{"x1":785,"y1":778,"x2":786,"y2":778,"stroke":69,"strokeWidth":780,"style":781,"markerEnd":782},"285.6","323.6",[257,788],{"x1":789,"y1":778,"x2":790,"y2":778,"stroke":69,"strokeWidth":780,"style":781,"markerEnd":782},"434.4","472.4",[257,792],{"x1":793,"y1":778,"x2":794,"y2":778,"stroke":69,"strokeWidth":780,"style":781,"markerEnd":782},"583.2","621.2",[241,796,798],{"id":797},"_3-report-the-worst-interaction-per-route-not-just-per-session","3. Report the worst interaction per route, not just per session",[246,800,802],{"className":248,"code":801,"language":250,"meta":251,"style":251},"const worstByRoute = new Map();\nnew PerformanceObserver((list) => {\n  for (const e of list.getEntries()) {\n    if (!e.interactionId) continue;\n    const route = routeAt(e.startTime);\n    const prev = worstByRoute.get(route);\n    if (!prev || e.duration > prev.duration) {\n      worstByRoute.set(route, { duration: e.duration, name: e.name,\n        target: e.target?.closest('[data-component]')?.dataset.component || e.target?.tagName });\n    }\n  }\n}).observe({ type: 'event', durationThreshold: 40, buffered: true });\n\naddEventListener('visibilitychange', () => {\n  if (document.visibilityState === 'hidden' && worstByRoute.size) {\n    navigator.sendBeacon('\u002Frum\u002Finp-routes', JSON.stringify([...worstByRoute]));\n  }\n});\n\u002F\u002F trade-off: durationThreshold 40 drops the fastest interactions, so a route\n\u002F\u002F with only fast ones reports nothing. That is fine for finding problems but\n\u002F\u002F means \"no row\" ≠ \"no traffic\" — count interactions separately if you need it.\n",[30,803,804,823,843,866,884,899,917,940,951,972,977,982,1011,1015,1032,1051,1084,1088,1093,1099,1105],{"__ignoreMap":251},[255,805,806,809,812,814,817,820],{"class":257,"line":258},[255,807,808],{"class":268},"const",[255,810,811],{"class":275}," worstByRoute",[255,813,279],{"class":268},[255,815,816],{"class":268}," new",[255,818,819],{"class":299}," Map",[255,821,822],{"class":282},"();\n",[255,824,825,828,831,834,837,839,841],{"class":257,"line":265},[255,826,827],{"class":268},"new",[255,829,830],{"class":299}," PerformanceObserver",[255,832,833],{"class":282},"((",[255,835,836],{"class":306},"list",[255,838,437],{"class":282},[255,840,571],{"class":268},[255,842,574],{"class":282},[255,844,845,847,849,851,854,857,860,863],{"class":257,"line":291},[255,846,396],{"class":268},[255,848,399],{"class":282},[255,850,808],{"class":268},[255,852,853],{"class":275}," e",[255,855,856],{"class":268}," of",[255,858,859],{"class":282}," list.",[255,861,862],{"class":299},"getEntries",[255,864,865],{"class":282},"()) {\n",[255,867,868,871,873,876,879,882],{"class":257,"line":313},[255,869,870],{"class":268},"    if",[255,872,399],{"class":282},[255,874,875],{"class":268},"!",[255,877,878],{"class":282},"e.interactionId) ",[255,880,881],{"class":268},"continue",[255,883,529],{"class":282},[255,885,886,889,892,894,896],{"class":257,"line":331},[255,887,888],{"class":268},"    const",[255,890,891],{"class":275}," route",[255,893,279],{"class":268},[255,895,383],{"class":299},[255,897,898],{"class":282},"(e.startTime);\n",[255,900,901,903,906,908,911,914],{"class":257,"line":370},[255,902,888],{"class":268},[255,904,905],{"class":275}," prev",[255,907,279],{"class":268},[255,909,910],{"class":282}," worstByRoute.",[255,912,913],{"class":299},"get",[255,915,916],{"class":282},"(route);\n",[255,918,919,921,923,925,928,931,934,937],{"class":257,"line":376},[255,920,870],{"class":268},[255,922,399],{"class":282},[255,924,875],{"class":268},[255,926,927],{"class":282},"prev ",[255,929,930],{"class":268},"||",[255,932,933],{"class":282}," e.duration ",[255,935,936],{"class":268},">",[255,938,939],{"class":282}," prev.duration) {\n",[255,941,942,945,948],{"class":257,"line":393},[255,943,944],{"class":282},"      worstByRoute.",[255,946,947],{"class":299},"set",[255,949,950],{"class":282},"(route, { duration: e.duration, name: e.name,\n",[255,952,953,956,959,961,964,967,969],{"class":257,"line":458},[255,954,955],{"class":282},"        target: e.target?.",[255,957,958],{"class":299},"closest",[255,960,303],{"class":282},[255,962,963],{"class":525},"'[data-component]'",[255,965,966],{"class":282},")?.dataset.component ",[255,968,930],{"class":268},[255,970,971],{"class":282}," e.target?.tagName });\n",[255,973,974],{"class":257,"line":472},[255,975,976],{"class":282},"    }\n",[255,978,979],{"class":257,"line":477},[255,980,981],{"class":282},"  }\n",[255,983,984,987,990,993,996,999,1002,1005,1008],{"class":257,"line":483},[255,985,986],{"class":282},"}).",[255,988,989],{"class":299},"observe",[255,991,992],{"class":282},"({ type: ",[255,994,995],{"class":525},"'event'",[255,997,998],{"class":282},", durationThreshold: ",[255,1000,1001],{"class":275},"40",[255,1003,1004],{"class":282},", buffered: ",[255,1006,1007],{"class":275},"true",[255,1009,1010],{"class":282}," });\n",[255,1012,1013],{"class":257,"line":489},[255,1014,549],{"emptyLinePlaceholder":548},[255,1016,1017,1020,1022,1025,1028,1030],{"class":257,"line":654},[255,1018,1019],{"class":299},"addEventListener",[255,1021,303],{"class":282},[255,1023,1024],{"class":525},"'visibilitychange'",[255,1026,1027],{"class":282},", () ",[255,1029,571],{"class":268},[255,1031,574],{"class":282},[255,1033,1034,1036,1039,1042,1045,1048],{"class":257,"line":660},[255,1035,334],{"class":268},[255,1037,1038],{"class":282}," (document.visibilityState ",[255,1040,1041],{"class":268},"===",[255,1043,1044],{"class":525}," 'hidden'",[255,1046,1047],{"class":268}," &&",[255,1049,1050],{"class":282}," worstByRoute.size) {\n",[255,1052,1053,1056,1059,1061,1064,1066,1069,1072,1075,1078,1081],{"class":257,"line":666},[255,1054,1055],{"class":282},"    navigator.",[255,1057,1058],{"class":299},"sendBeacon",[255,1060,303],{"class":282},[255,1062,1063],{"class":525},"'\u002Frum\u002Finp-routes'",[255,1065,359],{"class":282},[255,1067,1068],{"class":275},"JSON",[255,1070,1071],{"class":282},".",[255,1073,1074],{"class":299},"stringify",[255,1076,1077],{"class":282},"([",[255,1079,1080],{"class":268},"...",[255,1082,1083],{"class":282},"worstByRoute]));\n",[255,1085,1086],{"class":257,"line":672},[255,1087,981],{"class":282},[255,1089,1091],{"class":257,"line":1090},18,[255,1092,657],{"class":282},[255,1094,1096],{"class":257,"line":1095},19,[255,1097,1098],{"class":261},"\u002F\u002F trade-off: durationThreshold 40 drops the fastest interactions, so a route\n",[255,1100,1102],{"class":257,"line":1101},20,[255,1103,1104],{"class":261},"\u002F\u002F with only fast ones reports nothing. That is fine for finding problems but\n",[255,1106,1108],{"class":257,"line":1107},21,[255,1109,1110],{"class":261},"\u002F\u002F means \"no row\" ≠ \"no traffic\" — count interactions separately if you need it.\n",[15,1112,1113],{},"Expected outcome: every route visited in a session gets its own worst-interaction record, so p75 per route can be computed directly.",[241,1115,1117],{"id":1116},"_4-attribute-shared-components-to-the-routes-state","4. Attribute shared components to the route's state",[15,1119,1120,1121,1124,1125,1127,1128,1131],{},"For components present on every route, add the route to their component tag so a slow cart drawer on ",[30,1122,1123],{},"\u002Fcheckout"," is distinguishable from the same drawer on ",[30,1126,174],{},". A ",[30,1129,1130],{},"data-component"," attribute on the component's root, combined with the route from step 1, is usually enough.",[15,1133,1134],{},"Expected outcome: the \"header search is slow everywhere\" story splits into \"header search is slow on \u002Fsearch where it filters 5,000 results\", which is actionable.",[15,1136,1137],{},[40,1138,47,1141,47,1144,47,1147,47,1149,47,1152,47,1154,47,1162,47,1169,47,1176,47,1181,47,1185,47,1189,47,1193,47,1196,47,1201,47,1205,47,1209,47,1213,47,1217,47],{"viewBox":1139,"width":43,"role":44,"ariaLabel":1140,"style":46},"0 0 760 216","Bar chart of p75 INP per route template after route tagging, showing checkout far above the 200 millisecond threshold while the landing routes pass.",[49,1142],{"className":1143,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,1145,1146],{},"INP p75 per route after re-attribution",[60,1148,1140],{},[49,1150],{"x":64,"y":64,"width":65,"height":1151,"rx":67,"fill":68,"stroke":69,"style":70},"214",[72,1153,1146],{"x":74,"y":75,"fill":69,"style":76},[257,1155],{"x1":1156,"y1":1157,"x2":1156,"y2":1158,"stroke":84,"strokeWidth":780,"strokeDashArray":1159,"style":1161},"445.9","72.0","194.0",[1160,1160],"4","stroke-opacity:0.9",[72,1163,1168],{"x":1164,"y":1165,"fill":69,"style":1166,"textAnchor":1167},"121.2","92.0","font-size:13px","end","\u002Fshoes (landing)",[49,1170],{"x":1171,"y":722,"width":1172,"height":1173,"rx":1174,"fill":123,"stroke":123,"style":1175},"133.2","150.1","19","3","fill-opacity:0.55;stroke-opacity:0.9",[72,1177,1180],{"x":1178,"y":1165,"fill":69,"style":1179},"289.3","font-size:12px;font-weight:600","96ms",[72,1182,1184],{"x":1164,"y":1183,"fill":69,"style":1166,"textAnchor":1167},"123.0","\u002Fshoes\u002F:id",[49,1186],{"x":1171,"y":1187,"width":1188,"height":1173,"rx":1174,"fill":123,"stroke":123,"style":1175},"109.0","231.4",[72,1190,1192],{"x":1191,"y":1183,"fill":69,"style":1179},"370.6","148ms",[72,1194,1123],{"x":1164,"y":1195,"fill":69,"style":1166,"textAnchor":1167},"154.0",[49,1197],{"x":1171,"y":1198,"width":1199,"height":1173,"rx":1174,"fill":83,"stroke":84,"style":1200},"140.0","534.8","fill-opacity:0.7;stroke-opacity:0.9",[72,1202,1204],{"x":1203,"y":1195,"fill":69,"style":1179},"674.0","342ms",[72,1206,1208],{"x":1164,"y":1207,"fill":69,"style":1166,"textAnchor":1167},"185.0","\u002Faccount",[49,1210],{"x":1171,"y":1211,"width":1212,"height":1173,"rx":1174,"fill":123,"stroke":123,"style":1175},"171.0","189.2",[72,1214,1216],{"x":1215,"y":1207,"fill":69,"style":1179},"328.4","121ms",[72,1218,1222],{"x":1156,"y":1219,"fill":1220,"style":1221,"textAnchor":724},"68.0","#8a6608","font-size:12px;font-weight:700","200ms INP",[151,1224,1226],{"id":1225},"verification","Verification",[15,1228,1229,1230,1233],{},"Introduce a deliberate regression on one route in staging (a ",[30,1231,1232],{},"150ms"," synchronous loop in a button handler) and confirm that only that route's p75 moves in your per-route report while the landing route stays flat. Then compare a week of production data: the sum of interactions across route rows should roughly equal the total interaction count, and the landing route's INP should drop to its own true value. That drop is not an improvement in user experience — it is the end of a misattribution — so annotate it on dashboards to avoid celebrating it.",[151,1235,1237],{"id":1236},"using-the-data-to-prioritise","Using the Data to Prioritise",[15,1239,1240,1241,1243],{},"Route-level INP lets you weight fixes by business impact. Multiply each route's share of failing interactions (above ",[30,1242,32],{},") by its traffic and its conversion importance. A checkout route with 5% of traffic and 60% of failing interactions is almost always the first fix, even though it never appears in Search Console. Communicate this clearly to stakeholders who track CrUX: fixing checkout will improve the landing pages' field INP, because those sessions will stop carrying checkout's slow interactions home.",[151,1245,1247],{"id":1246},"common-mistakes-when-adding-route-attribution","Common Mistakes When Adding Route Attribution",[15,1249,1250,1253,1254,1257,1258,1261],{},[162,1251,1252],{},"Capturing the route when the beacon is sent."," The beacon goes out on ",[30,1255,1256],{},"visibilitychange",", by which time the user may be three routes further on. Always resolve the route from the interaction's own timestamp, never from ",[30,1259,1260],{},"location"," at send time.",[15,1263,1264,47,1267,1270,1271,1274],{},[162,1265,1266],{},"Using concrete URLs as the route key.",[30,1268,1269],{},"\u002Fproduct\u002F12345"," and ",[30,1272,1273],{},"\u002Fproduct\u002F12346"," share code and should share a row. Use the router's matched pattern; fall back to a regex that collapses numeric and UUID segments if your router does not expose patterns.",[15,1276,1277,1280,1281,1284],{},[162,1278,1279],{},"Forgetting the landing route's hydration."," Interactions in the first seconds of a hard load overlap hydration and are slower for reasons unrelated to that view's code. Tag interactions that occurred before hydration completed (",[30,1282,1283],{},"performance.getEntriesByName('hydrated')"," or a framework hook) so they can be analysed separately.",[15,1286,1287,1290],{},[162,1288,1289],{},"Splitting the sample too thin."," Routes with a few dozen interactions a day produce a p75 that swings wildly. Report a minimum sample size alongside each route's value and roll rare routes into an \"other\" bucket for alerting purposes.",[151,1292,1294],{"id":1293},"faq","FAQ",[1296,1297,1300,1304],"details",{"className":1298},[1299],"faq-item",[1301,1302,1303],"summary",{},"Does fixing a deep route really improve the landing page's CrUX INP?",[15,1305,1306],{},"Yes, for sessions that pass through it. CrUX records the session's worst interaction against the landing URL, so removing the slowest interaction from the deep route lowers the INP of every session that visited it. The effect is proportional to how many sessions reach that route.",[1296,1308,1310,1313],{"className":1309},[1299],[1301,1311,1312],{},"What about interactions during the navigation itself?",[15,1314,1315,1316,1318],{},"The click that triggers a navigation happens on the old route; its processing may include starting the new one. Attribute it to the old route by ",[30,1317,754],{},", but record the destination too. Slow navigation clicks usually mean the router handler does synchronous work — rendering the next route before yielding — which is fixed in the router, not the destination view.",[151,1320,1322],{"id":1321},"related","Related",[156,1324,1325,1332,1339],{},[159,1326,1327,1331],{},[19,1328,1330],{"href":1329},"\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 with the attribution build"," — the component-level half of the same attribution problem.",[159,1333,1334,1338],{},[19,1335,1337],{"href":1336},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fwhy-spa-cls-accumulates-across-routes\u002F","Why SPA CLS accumulates across routes"," — the equivalent fix for layout shift.",[159,1340,1341,1345],{},[19,1342,1344],{"href":1343},"\u002Fcore-web-vitals-measurement\u002Foptimizing-first-input-delay-fid\u002Fimproving-inp-for-complex-single-page-applications\u002F","Improving INP for complex single page applications"," — what to do once you know which route is slow.",[1347,1348,1350],"script",{"type":1349},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Attribute INP to the Route That Actually Caused It\",\n  \"description\": \"Stop blaming the landing page for slow interactions on other views by tagging every interaction with the route that was active.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Keep a timestamped route history\",\n      \"text\": \"Call pushRoute(to.matched.at(-1).path) (Vue Router) or the equivalent template in your router's after-navigation hook.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Tag INP with the route at the interaction's start time\",\n      \"text\": \"onINP(({ value, attribution }) => { report({ metric: 'INP', value: Math.round(value), route: routeAt(attribution.interactionTime), \u002F\u002F where it happened landing: history[0].route, \u002F\u002F where the session began target: attribution.interactionTarget, phase: { input: attribution.inputDelay, processing: att\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Report the worst interaction per route, not just per session\",\n      \"text\": \"addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden' && worstByRoute.size) { navigator.sendBeacon('\u002Frum\u002Finp-routes', JSON.stringify([...worstByRoute])); } }); \u002F\u002F trade-off: durationThreshold 40 drops the fastest interactions, so a route \u002F\u002F with only fast ones report\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Attribute shared components to the route's state\",\n      \"text\": \"For components present on every route, add the route to their component tag so a slow cart drawer on \u002Fcheckout is distinguishable from the same drawer on \u002Fshoes.\"\n    }\n  ]\n}\n",[1347,1352,1353],{"type":1349},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Attribute INP to the Route That Actually Caused It\",\n  \"description\": \"Stop blaming the landing page for slow interactions on other views by tagging every interaction with the route that was active.\",\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\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa\u002F\"\n  }\n}\n",[1347,1355,1356],{"type":1349},"\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\": \"Core Web Vitals & Measurement\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Soft Navigations & SPA Metrics\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Attributing INP to the Right Route in an SPA\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa\u002F\"\n    }\n  ]\n}\n",[1358,1359,1360],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}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 .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}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);}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}",{"title":251,"searchDepth":265,"depth":265,"links":1362},[1363,1364,1365,1371,1372,1373,1374,1375],{"id":153,"depth":265,"text":154},{"id":205,"depth":265,"text":206},{"id":238,"depth":265,"text":239,"children":1366},[1367,1368,1369,1370],{"id":243,"depth":291,"text":244},{"id":505,"depth":291,"text":506},{"id":797,"depth":291,"text":798},{"id":1116,"depth":291,"text":1117},{"id":1225,"depth":265,"text":1226},{"id":1236,"depth":265,"text":1237},{"id":1246,"depth":265,"text":1247},{"id":1293,"depth":265,"text":1294},{"id":1321,"depth":265,"text":1322},"Stop blaming the landing page for slow interactions on other views by tagging every interaction with the route that was active.","md",{"slug":1379,"type":1380,"breadcrumb":1381,"datePublished":1389,"dateModified":1389},"attributing-inp-to-the-right-route-in-an-spa","article",[1382,1385,1386,1387],{"name":1383,"url":1384},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1388},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa\u002F","2026-10-06","\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa",{"title":5,"description":1392},"INP in single-page apps is charged to the landing URL. Tag each interaction with its active route to find the view that really fails the 200ms budget.","core-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa\u002Findex","ma3BrRsSNfZhlBDmq4YZMsHyotWv8VEHN65C7skfAck",[1396,1399],{"title":22,"path":1397,"stem":1398,"children":-1},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics","core-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Findex",{"title":1400,"path":1401,"stem":1402,"children":-1},"Measuring LCP After Client-Side Route Changes","\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fmeasuring-lcp-after-client-side-route-changes","core-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fmeasuring-lcp-after-client-side-route-changes\u002Findex",1791308074537]