[{"data":1,"prerenderedAt":2165},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics":2156},{"id":4,"title":5,"body":6,"description":2138,"extension":2139,"meta":2140,"navigation":459,"path":2150,"seo":2151,"stem":2154,"__hash__":2155},"content\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Findex.md","Soft Navigations & SPA Metrics",{"type":7,"value":8,"toc":2123},"minimark",[9,14,33,52,67,207,212,215,226,232,238,300,304,372,376,379,643,650,654,657,757,761,764,806,810,813,1193,1200,1204,1211,1306,1399,1402,1406,1416,1429,1439,1445,1460,1464,1477,1615,1749,1753,1756,1799,1826,1839,1871,1881,1884,1997,2001,2013,2027,2036,2045,2049,2074,2078,2108,2113,2116,2119],[10,11,13],"h1",{"id":12},"soft-navigations-and-spa-metrics-measuring-core-web-vitals-per-route","Soft Navigations and SPA Metrics: Measuring Core Web Vitals Per Route",[15,16,17,18,23,24,28,29,32],"p",{},"This topic sits within ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement"," and deals with the measurement gap every single-page application eventually runs into: the browser's metrics are defined per ",[25,26,27],"em",{},"document",", but your users experience the site per ",[25,30,31],{},"route",".",[15,34,35,36,40,41,44,45,48,49,51],{},"When a user lands on ",[37,38,39],"code",{},"\u002Fproducts",", clicks through to ",[37,42,43],{},"\u002Fproducts\u002F42",", then to ",[37,46,47],{},"\u002Fcart",", the browser sees one page load. Largest Contentful Paint is recorded once — for the landing view — and stops at the first interaction. Interaction to Next Paint takes the worst interaction across all three views and reports it against ",[37,50,39],{},". Cumulative Layout Shift accumulates its session windows across all three routes and blames the landing URL too. Chrome's User Experience Report (CrUX) does the same. The result is a dashboard where the landing page looks guilty for problems on the cart, and the cart never appears at all.",[15,53,54,55,58,59,62,63,66],{},"The thresholds do not change — LCP under ",[37,56,57],{},"2.5s",", INP under ",[37,60,61],{},"200ms",", CLS under ",[37,64,65],{},"0.1"," at the 75th percentile — but the unit they apply to does. This topic covers how to measure route changes (\"soft navigations\") as first-class page views, how the experimental Soft Navigations API in Chromium approaches the problem, and how to make your own RUM attribute every metric to the route that caused it.",[15,68,69],{},[70,71,77,78,77,85,77,89,77,92,77,101,77,107,77,112,77,121,77,127,77,134,77,137,77,142,77,145,77,149,77,153,77,157,77,161,77,166,77,173,77,179,77,183,77,187,77,190,77,194,77,198,77,202,77],"svg",{"viewBox":72,"width":73,"role":74,"ariaLabel":75,"style":76},"0 0 760 242","100%","img","A single page visit spanning three client-side routes, showing that LCP is only measured on the landing route while INP and CLS accumulate across all of them.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[79,80],"rect",{"className":81,"x":83,"y":83,"width":73,"height":73,"fill":84},[82],"svg-canvas","0","#ffffff",[86,87,88],"title",{},"Three routes, one document, one set of metrics",[90,91,75],"desc",{},[79,93],{"x":94,"y":94,"width":95,"height":96,"rx":97,"fill":98,"stroke":99,"style":100},"1","758","240","10","none","currentColor","stroke-opacity:0.18",[102,103,88],"text",{"x":104,"y":105,"fill":99,"style":106},"28.0","34.0","font-size:16px;font-weight:700",[102,108,111],{"x":104,"y":109,"fill":99,"style":110},"74.0","font-size:12.5px;font-weight:700","Routes",[79,113],{"x":114,"y":115,"width":116,"height":117,"rx":118,"fill":119,"stroke":119,"style":120},"105.8","56.0","184.8","26.0","3","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[102,122,39],{"x":123,"y":124,"fill":99,"style":125,"textAnchor":126},"198.2","73.5","font-size:11.5px","middle",[79,128],{"x":129,"y":115,"width":130,"height":117,"rx":118,"fill":131,"stroke":132,"style":133},"292.1","236.5","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[102,135,43],{"x":136,"y":124,"fill":99,"style":125,"textAnchor":126},"410.3",[79,138],{"x":139,"y":115,"width":140,"height":117,"rx":118,"fill":99,"stroke":99,"style":141},"530.1","195.1","fill-opacity:0.06;stroke-opacity:0.4",[102,143,47],{"x":144,"y":124,"fill":99,"style":125,"textAnchor":126},"627.7",[102,146,148],{"x":104,"y":147,"fill":99,"style":110},"112.0","LCP",[79,150],{"x":114,"y":151,"width":152,"height":117,"rx":118,"fill":119,"stroke":119,"style":120},"94.0","29.5",[102,154,156],{"x":104,"y":155,"fill":99,"style":110},"150.0","INP + CLS",[79,158],{"x":114,"y":159,"width":160,"height":117,"rx":118,"fill":99,"stroke":99,"style":141},"132.0","619.5",[102,162,165],{"x":163,"y":164,"fill":99,"style":125,"textAnchor":126},"415.5","149.5","accumulated across all routes",[167,168],"line",{"x1":169,"y1":170,"x2":171,"y2":170,"stroke":99,"style":172},"105.0","172.0","726.0","stroke-opacity:0.4",[102,174,178],{"x":175,"y":176,"fill":177,"style":125,"textAnchor":126},"111.0","189.0","#51617a","0s",[102,180,182],{"x":181,"y":176,"fill":177,"style":125,"textAnchor":126},"208.5","10s",[102,184,186],{"x":185,"y":176,"fill":177,"style":125,"textAnchor":126},"312.0","20s",[102,188,189],{"x":163,"y":176,"fill":177,"style":125,"textAnchor":126},"30s",[102,191,193],{"x":192,"y":176,"fill":177,"style":125,"textAnchor":126},"519.0","40s",[102,195,197],{"x":196,"y":176,"fill":177,"style":125,"textAnchor":126},"622.5","50s",[102,199,201],{"x":200,"y":176,"fill":177,"style":125,"textAnchor":126},"723.0","60s",[102,203,206],{"x":104,"y":204,"fill":177,"style":205},"220.0","font-size:12.5px","Without soft-navigation handling, problems on \u002Fproducts\u002F42 and \u002Fcart are charged to the landing route.",[208,209,211],"h2",{"id":210},"the-measurement-failure-this-topic-fixes","The Measurement Failure This Topic Fixes",[15,213,214],{},"There are three distinct symptoms, and teams usually notice them in this order.",[15,216,217,221,222,225],{},[218,219,220],"strong",{},"Landing pages that are blamed for other pages."," A category page shows INP of ",[37,223,224],{},"310ms"," at p75 in CrUX, but every interaction on it is cheap in the lab. The slow interaction is the \"add to cart\" button on the product detail view the user reached by a soft navigation. Field data charges it to the URL in the address bar at load time.",[15,227,228,231],{},[218,229,230],{},"Routes that never appear."," Product detail pages reached only by in-app navigation have no CrUX record, no LCP, and no INP of their own. If 70% of product views are soft navigations, 70% of your most commercially important views are unmeasured.",[15,233,234,237],{},[218,235,236],{},"CLS that grows with session length."," CLS reports the largest burst of layout shift in any session window (shifts less than one second apart, capped at five seconds). In a long SPA session, there are many windows, and the chance that one of them contains a bad burst grows with time on site. Long engaged sessions look worse than bounces.",[15,239,240],{},[70,241,77,244,77,247,77,250,77,252,77,255,77,257,77,264,77,270,77,275,77,279,77,284,77,288,77,291,77,296,77],{"viewBox":242,"width":73,"role":74,"ariaLabel":243,"style":76},"0 0 760 163","Bar chart showing that most product detail views in a single-page storefront arrive by soft navigation and are therefore unmeasured by default.",[79,245],{"className":246,"x":83,"y":83,"width":73,"height":73,"fill":84},[82],[86,248,249],{},"Share of product views by navigation type (typical storefront)",[90,251,243],{},[79,253],{"x":94,"y":94,"width":95,"height":254,"rx":97,"fill":98,"stroke":99,"style":100},"161",[102,256,249],{"x":104,"y":105,"fill":99,"style":106},[102,258,263],{"x":259,"y":260,"fill":99,"style":261,"textAnchor":262},"197.6","70.0","font-size:13px","end","Hard load (measured)",[79,265],{"x":266,"y":115,"width":267,"height":268,"rx":118,"fill":119,"stroke":119,"style":269},"209.6","200.5","19","fill-opacity:0.55;stroke-opacity:0.9",[102,271,274],{"x":272,"y":260,"fill":99,"style":273},"416.2","font-size:12px;font-weight:600","28%",[102,276,278],{"x":259,"y":277,"fill":99,"style":261,"textAnchor":262},"101.0","Soft navigation (unmeasured)",[79,280],{"x":266,"y":281,"width":282,"height":268,"rx":118,"fill":131,"stroke":132,"style":283},"87.0","458.4","fill-opacity:0.7;stroke-opacity:0.9",[102,285,287],{"x":286,"y":277,"fill":99,"style":273},"674.0","64%",[102,289,290],{"x":259,"y":159,"fill":99,"style":261,"textAnchor":262},"Back\u002Fforward restore",[79,292],{"x":266,"y":293,"width":294,"height":268,"rx":118,"fill":99,"stroke":99,"style":295},"118.0","57.3","fill-opacity:0.7;stroke-opacity:0.4",[102,297,299],{"x":298,"y":159,"fill":99,"style":273},"272.9","8%",[208,301,303],{"id":302},"prerequisites","Prerequisites",[305,306,307,334,348,358],"ul",{},[308,309,310,313,314,317,318,321,322,325,326,329,330,333],"li",{},[218,311,312],{},"A router that exposes navigation lifecycle hooks"," — ",[37,315,316],{},"router.afterEach"," in Vue Router, ",[37,319,320],{},"useEffect"," on location in React Router, ",[37,323,324],{},"onNavigate","\u002F",[37,327,328],{},"afterNavigate"," in SvelteKit, or the Navigation API's ",[37,331,332],{},"navigatesuccess"," event where available.",[308,335,336,343,344,347],{},[218,337,338,339,342],{},"The ",[37,340,341],{},"web-vitals"," library v4+",", and familiarity with its attribution build. For the experimental soft-navigation reporting, the library exposes an opt-in ",[37,345,346],{},"reportSoftNavs"," option on Chromium builds where the API is enabled.",[308,349,350,353,354,357],{},[218,351,352],{},"Chromium with the Soft Navigations API enabled"," for lab work: ",[37,355,356],{},"chrome:\u002F\u002Fflags\u002F#enable-experimental-web-platform-features",", or an origin-trial token on your domain for field testing. Treat it as experimental — its heuristics and entry shapes have changed between trials.",[308,359,360,366,367,371],{},[218,361,362,363,365],{},"A RUM pipeline that can carry a ",[37,364,31],{}," dimension"," separate from the document URL. The ",[19,368,370],{"href":369},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002F","RUM beacons and field data collection"," topic covers the basics.",[208,373,375],{"id":374},"_1-environment-setup-define-what-counts-as-a-soft-navigation","1. Environment Setup: Define What Counts as a Soft Navigation",[15,377,378],{},"Before measuring anything, agree on a definition. Chromium's experimental heuristic requires three things: a user interaction, a same-document URL change via the History or Navigation API, and a DOM modification that results in a paint. That is a sound default. Filtering on the user interaction excludes programmatic redirects and query-string updates from filters or tabs that you probably do not want counted as page views.",[380,381,386],"pre",{"className":382,"code":383,"language":384,"meta":385,"style":385},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F route-tracker.js — a router-agnostic soft-navigation boundary.\nlet currentRoute = location.pathname;\nlet navStart = performance.now();\nconst listeners = new Set();\n\nexport function onSoftNavigation(fn) { listeners.add(fn); }\n\nexport function markSoftNavigation(nextRoute) {\n  if (nextRoute === currentRoute) return;          \u002F\u002F ignore hash\u002Fquery-only changes\n  const prev = { route: currentRoute, start: navStart };\n  currentRoute = nextRoute;\n  navStart = performance.now();\n  performance.mark('soft-nav', { detail: { route: nextRoute } });\n  listeners.forEach((fn) => fn(prev, { route: nextRoute, start: navStart }));\n}\n\u002F\u002F trade-off: keying on pathname alone treats \u002Fsearch?q=a and \u002Fsearch?q=b as the\n\u002F\u002F same view. For search-heavy apps, include the parameters that change the\n\u002F\u002F rendered template, but never free-text values (cardinality and privacy).\n","javascript","",[37,387,388,396,413,433,454,461,489,494,512,536,550,561,575,593,619,625,631,637],{"__ignoreMap":385},[389,390,392],"span",{"class":167,"line":391},1,[389,393,395],{"class":394},"sjfSM","\u002F\u002F route-tracker.js — a router-agnostic soft-navigation boundary.\n",[389,397,399,403,407,410],{"class":167,"line":398},2,[389,400,402],{"class":401},"sPARh","let",[389,404,406],{"class":405},"saISM"," currentRoute ",[389,408,409],{"class":401},"=",[389,411,412],{"class":405}," location.pathname;\n",[389,414,416,418,421,423,426,430],{"class":167,"line":415},3,[389,417,402],{"class":401},[389,419,420],{"class":405}," navStart ",[389,422,409],{"class":401},[389,424,425],{"class":405}," performance.",[389,427,429],{"class":428},"smZ65","now",[389,431,432],{"class":405},"();\n",[389,434,436,439,443,446,449,452],{"class":167,"line":435},4,[389,437,438],{"class":401},"const",[389,440,442],{"class":441},"sPXB4"," listeners",[389,444,445],{"class":401}," =",[389,447,448],{"class":401}," new",[389,450,451],{"class":428}," Set",[389,453,432],{"class":405},[389,455,457],{"class":167,"line":456},5,[389,458,460],{"emptyLinePlaceholder":459},true,"\n",[389,462,464,467,470,473,476,480,483,486],{"class":167,"line":463},6,[389,465,466],{"class":401},"export",[389,468,469],{"class":401}," function",[389,471,472],{"class":428}," onSoftNavigation",[389,474,475],{"class":405},"(",[389,477,479],{"class":478},"sQw3B","fn",[389,481,482],{"class":405},") { listeners.",[389,484,485],{"class":428},"add",[389,487,488],{"class":405},"(fn); }\n",[389,490,492],{"class":167,"line":491},7,[389,493,460],{"emptyLinePlaceholder":459},[389,495,497,499,501,504,506,509],{"class":167,"line":496},8,[389,498,466],{"class":401},[389,500,469],{"class":401},[389,502,503],{"class":428}," markSoftNavigation",[389,505,475],{"class":405},[389,507,508],{"class":478},"nextRoute",[389,510,511],{"class":405},") {\n",[389,513,515,518,521,524,527,530,533],{"class":167,"line":514},9,[389,516,517],{"class":401},"  if",[389,519,520],{"class":405}," (nextRoute ",[389,522,523],{"class":401},"===",[389,525,526],{"class":405}," currentRoute) ",[389,528,529],{"class":401},"return",[389,531,532],{"class":405},";          ",[389,534,535],{"class":394},"\u002F\u002F ignore hash\u002Fquery-only changes\n",[389,537,539,542,545,547],{"class":167,"line":538},10,[389,540,541],{"class":401},"  const",[389,543,544],{"class":441}," prev",[389,546,445],{"class":401},[389,548,549],{"class":405}," { route: currentRoute, start: navStart };\n",[389,551,553,556,558],{"class":167,"line":552},11,[389,554,555],{"class":405},"  currentRoute ",[389,557,409],{"class":401},[389,559,560],{"class":405}," nextRoute;\n",[389,562,564,567,569,571,573],{"class":167,"line":563},12,[389,565,566],{"class":405},"  navStart ",[389,568,409],{"class":401},[389,570,425],{"class":405},[389,572,429],{"class":428},[389,574,432],{"class":405},[389,576,578,581,584,586,590],{"class":167,"line":577},13,[389,579,580],{"class":405},"  performance.",[389,582,583],{"class":428},"mark",[389,585,475],{"class":405},[389,587,589],{"class":588},"sZ8jY","'soft-nav'",[389,591,592],{"class":405},", { detail: { route: nextRoute } });\n",[389,594,596,599,602,605,607,610,613,616],{"class":167,"line":595},14,[389,597,598],{"class":405},"  listeners.",[389,600,601],{"class":428},"forEach",[389,603,604],{"class":405},"((",[389,606,479],{"class":478},[389,608,609],{"class":405},") ",[389,611,612],{"class":401},"=>",[389,614,615],{"class":428}," fn",[389,617,618],{"class":405},"(prev, { route: nextRoute, start: navStart }));\n",[389,620,622],{"class":167,"line":621},15,[389,623,624],{"class":405},"}\n",[389,626,628],{"class":167,"line":627},16,[389,629,630],{"class":394},"\u002F\u002F trade-off: keying on pathname alone treats \u002Fsearch?q=a and \u002Fsearch?q=b as the\n",[389,632,634],{"class":167,"line":633},17,[389,635,636],{"class":394},"\u002F\u002F same view. For search-heavy apps, include the parameters that change the\n",[389,638,640],{"class":167,"line":639},18,[389,641,642],{"class":394},"\u002F\u002F rendered template, but never free-text values (cardinality and privacy).\n",[15,644,645,646,649],{},"Wire ",[37,647,648],{},"markSoftNavigation"," into the router's after-navigation hook so it fires once the new route has committed, not when the click happens.",[208,651,653],{"id":652},"_2-capture-a-baseline-per-route","2. Capture a Baseline per Route",[15,655,656],{},"Before changing anything, split your existing RUM by the route active at the time each metric was finalised. For INP, record the route at the time of the worst interaction; for CLS, record which route each session window began in. Even this rough cut usually reveals that one or two in-app routes own most of the failures charged to the landing page.",[15,658,659],{},[70,660,77,663,77,666,77,669,77,671,77,688,77,691,77,693,77,698,77,703,77,708,77,711,77,715,77,718,77,721,77,724,77,728,77,731,77,734,77,738,77,741,77,749,77,753,77],{"viewBox":661,"width":73,"role":74,"ariaLabel":662,"style":76},"0 0 760 149","Four-step process from tagging each metric with the active route through to a per-route p75 table.",[79,664],{"className":665,"x":83,"y":83,"width":73,"height":73,"fill":84},[82],[86,667,668],{},"Baseline: re-attributing existing beacons to routes",[90,670,662],{},[672,673,674],"defs",{},[675,676,683],"marker",{"id":677,"viewBox":678,"refX":679,"refY":680,"markerWidth":681,"markerHeight":681,"orient":682},"fa120ab932","0 0 10 10","9","5","7","auto-start-reverse",[684,685],"path",{"d":686,"fill":99,"style":687},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[79,689],{"x":94,"y":94,"width":95,"height":690,"rx":97,"fill":98,"stroke":99,"style":100},"147",[102,692,668],{"x":104,"y":105,"fill":99,"style":106},[79,694],{"x":104,"y":115,"width":695,"height":696,"rx":697,"fill":119,"stroke":119,"style":120},"146.0","71.0","6",[102,699,702],{"x":277,"y":700,"fill":99,"style":701,"textAnchor":126},"78.0","font-size:13px;font-weight:700","Tag route",[102,704,707],{"x":277,"y":705,"fill":99,"style":706,"textAnchor":126},"96.0","font-size:12px","on every metric callback",[79,709],{"x":710,"y":115,"width":695,"height":696,"rx":697,"fill":119,"stroke":119,"style":120},"214.0",[102,712,714],{"x":713,"y":700,"fill":99,"style":701,"textAnchor":126},"287.0","Keep timestamps",[102,716,717],{"x":713,"y":705,"fill":99,"style":706,"textAnchor":126},"interaction and shift",[102,719,720],{"x":713,"y":147,"fill":99,"style":706,"textAnchor":126},"times",[79,722],{"x":723,"y":115,"width":695,"height":696,"rx":697,"fill":131,"stroke":132,"style":133},"400.0",[102,725,727],{"x":726,"y":700,"fill":99,"style":701,"textAnchor":126},"473.0","Group by route",[102,729,730],{"x":726,"y":705,"fill":99,"style":706,"textAnchor":126},"not by document URL",[79,732],{"x":733,"y":115,"width":695,"height":696,"rx":697,"fill":119,"stroke":119,"style":120},"586.0",[102,735,737],{"x":736,"y":700,"fill":99,"style":701,"textAnchor":126},"659.0","p75 per route",[102,739,740],{"x":736,"y":705,"fill":99,"style":706,"textAnchor":126},"compare with CrUX view",[167,742],{"x1":743,"y1":744,"x2":745,"y2":744,"stroke":99,"strokeWidth":746,"style":747,"markerEnd":748},"174.0","91.5","212.0","1.5","stroke-opacity:0.6","url(#fa120ab932)",[167,750],{"x1":751,"y1":744,"x2":752,"y2":744,"stroke":99,"strokeWidth":746,"style":747,"markerEnd":748},"360.0","398.0",[167,754],{"x1":755,"y1":744,"x2":756,"y2":744,"stroke":99,"strokeWidth":746,"style":747,"markerEnd":748},"546.0","584.0",[208,758,760],{"id":759},"_3-isolate-the-bottleneck-which-metric-which-route","3. Isolate the Bottleneck: Which Metric, Which Route",[15,762,763],{},"Treat each metric separately because each has a different measurement gap:",[305,765,766,775,785,795],{},[308,767,768,770,771,32],{},[218,769,148],{}," is not measured at all for soft navigations by the standard API. You need either the experimental soft-navigation LCP entries or your own \"route contentful paint\" built from element timing — see ",[19,772,774],{"href":773},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fmeasuring-lcp-after-client-side-route-changes\u002F","measuring LCP after client-side route changes",[308,776,777,780,781,32],{},[218,778,779],{},"INP"," is measured, but attributed to the document. The fix is attribution, not measurement — covered in ",[19,782,784],{"href":783},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa\u002F","attributing INP to the right route in an SPA",[308,786,787,790,791,32],{},[218,788,789],{},"CLS"," is measured and accumulated across routes; you need to reset or split its windows on navigation, explained in ",[19,792,794],{"href":793},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fwhy-spa-cls-accumulates-across-routes\u002F","why SPA CLS accumulates across routes",[308,796,797,800,801,805],{},[218,798,799],{},"Route transition time"," — click to new view painted — is not a Core Web Vital, but it is the metric users feel most in SPAs. ",[19,802,804],{"href":803},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Ftracking-route-transition-time-with-user-timing\u002F","Tracking route transition time with User Timing"," shows how to measure it consistently.",[208,807,809],{"id":808},"_4-apply-the-fix-per-route-metric-reporting","4. Apply the Fix: Per-Route Metric Reporting",[15,811,812],{},"The core change is to finalise and send each metric at every soft-navigation boundary, then start fresh for the next route.",[380,814,816],{"className":382,"code":815,"language":384,"meta":385,"style":385},"import { onSoftNavigation } from '.\u002Froute-tracker.js';\n\n\u002F\u002F Per-route accumulators, reset at every soft-navigation boundary.\nlet route = { inp: 0, interactions: 0, shifts: 0 };\n\n\u002F\u002F INP: observe Event Timing directly. web-vitals' onINP reports the PAGE-level\n\u002F\u002F worst value, which never decreases — useless for a later, faster route.\nnew PerformanceObserver((list) => {\n  for (const e of list.getEntries()) {\n    if (!e.interactionId) continue;\n    route.interactions += 1;\n    route.inp = Math.max(route.inp, e.duration);\n  }\n}).observe({ type: 'event', durationThreshold: 16, buffered: true });\n\n\u002F\u002F CLS: sum unexpected shifts that happened while this route was active.\nnew PerformanceObserver((list) => {\n  for (const e of list.getEntries()) if (!e.hadRecentInput) route.shifts += e.value;\n}).observe({ type: 'layout-shift', buffered: true });\n\nonSoftNavigation((prev) => {\n  send({ route: prev.route, inp: Math.round(route.inp), interactions: route.interactions,\n         cls: +route.shifts.toFixed(3) });\n  route = { inp: 0, interactions: 0, shifts: 0 };\n});\n\u002F\u002F trade-off: summing shifts per route overstates CLS on long-lived routes,\n\u002F\u002F because the official metric keeps only the worst session window. Use the\n\u002F\u002F window-splitting approach in the CLS guide when the route stays open for minutes.\n",[37,817,818,835,839,844,871,875,880,885,905,930,948,961,977,982,1011,1015,1020,1036,1070,1088,1093,1110,1125,1147,1169,1175,1181,1187],{"__ignoreMap":385},[389,819,820,823,826,829,832],{"class":167,"line":391},[389,821,822],{"class":401},"import",[389,824,825],{"class":405}," { onSoftNavigation } ",[389,827,828],{"class":401},"from",[389,830,831],{"class":588}," '.\u002Froute-tracker.js'",[389,833,834],{"class":405},";\n",[389,836,837],{"class":167,"line":398},[389,838,460],{"emptyLinePlaceholder":459},[389,840,841],{"class":167,"line":415},[389,842,843],{"class":394},"\u002F\u002F Per-route accumulators, reset at every soft-navigation boundary.\n",[389,845,846,848,851,853,856,858,861,863,866,868],{"class":167,"line":435},[389,847,402],{"class":401},[389,849,850],{"class":405}," route ",[389,852,409],{"class":401},[389,854,855],{"class":405}," { inp: ",[389,857,83],{"class":441},[389,859,860],{"class":405},", interactions: ",[389,862,83],{"class":441},[389,864,865],{"class":405},", shifts: ",[389,867,83],{"class":441},[389,869,870],{"class":405}," };\n",[389,872,873],{"class":167,"line":456},[389,874,460],{"emptyLinePlaceholder":459},[389,876,877],{"class":167,"line":463},[389,878,879],{"class":394},"\u002F\u002F INP: observe Event Timing directly. web-vitals' onINP reports the PAGE-level\n",[389,881,882],{"class":167,"line":491},[389,883,884],{"class":394},"\u002F\u002F worst value, which never decreases — useless for a later, faster route.\n",[389,886,887,890,893,895,898,900,902],{"class":167,"line":496},[389,888,889],{"class":401},"new",[389,891,892],{"class":428}," PerformanceObserver",[389,894,604],{"class":405},[389,896,897],{"class":478},"list",[389,899,609],{"class":405},[389,901,612],{"class":401},[389,903,904],{"class":405}," {\n",[389,906,907,910,913,915,918,921,924,927],{"class":167,"line":514},[389,908,909],{"class":401},"  for",[389,911,912],{"class":405}," (",[389,914,438],{"class":401},[389,916,917],{"class":441}," e",[389,919,920],{"class":401}," of",[389,922,923],{"class":405}," list.",[389,925,926],{"class":428},"getEntries",[389,928,929],{"class":405},"()) {\n",[389,931,932,935,937,940,943,946],{"class":167,"line":538},[389,933,934],{"class":401},"    if",[389,936,912],{"class":405},[389,938,939],{"class":401},"!",[389,941,942],{"class":405},"e.interactionId) ",[389,944,945],{"class":401},"continue",[389,947,834],{"class":405},[389,949,950,953,956,959],{"class":167,"line":552},[389,951,952],{"class":405},"    route.interactions ",[389,954,955],{"class":401},"+=",[389,957,958],{"class":441}," 1",[389,960,834],{"class":405},[389,962,963,966,968,971,974],{"class":167,"line":563},[389,964,965],{"class":405},"    route.inp ",[389,967,409],{"class":401},[389,969,970],{"class":405}," Math.",[389,972,973],{"class":428},"max",[389,975,976],{"class":405},"(route.inp, e.duration);\n",[389,978,979],{"class":167,"line":577},[389,980,981],{"class":405},"  }\n",[389,983,984,987,990,993,996,999,1002,1005,1008],{"class":167,"line":595},[389,985,986],{"class":405},"}).",[389,988,989],{"class":428},"observe",[389,991,992],{"class":405},"({ type: ",[389,994,995],{"class":588},"'event'",[389,997,998],{"class":405},", durationThreshold: ",[389,1000,1001],{"class":441},"16",[389,1003,1004],{"class":405},", buffered: ",[389,1006,1007],{"class":441},"true",[389,1009,1010],{"class":405}," });\n",[389,1012,1013],{"class":167,"line":621},[389,1014,460],{"emptyLinePlaceholder":459},[389,1016,1017],{"class":167,"line":627},[389,1018,1019],{"class":394},"\u002F\u002F CLS: sum unexpected shifts that happened while this route was active.\n",[389,1021,1022,1024,1026,1028,1030,1032,1034],{"class":167,"line":633},[389,1023,889],{"class":401},[389,1025,892],{"class":428},[389,1027,604],{"class":405},[389,1029,897],{"class":478},[389,1031,609],{"class":405},[389,1033,612],{"class":401},[389,1035,904],{"class":405},[389,1037,1038,1040,1042,1044,1046,1048,1050,1052,1055,1058,1060,1062,1065,1067],{"class":167,"line":639},[389,1039,909],{"class":401},[389,1041,912],{"class":405},[389,1043,438],{"class":401},[389,1045,917],{"class":441},[389,1047,920],{"class":401},[389,1049,923],{"class":405},[389,1051,926],{"class":428},[389,1053,1054],{"class":405},"()) ",[389,1056,1057],{"class":401},"if",[389,1059,912],{"class":405},[389,1061,939],{"class":401},[389,1063,1064],{"class":405},"e.hadRecentInput) route.shifts ",[389,1066,955],{"class":401},[389,1068,1069],{"class":405}," e.value;\n",[389,1071,1073,1075,1077,1079,1082,1084,1086],{"class":167,"line":1072},19,[389,1074,986],{"class":405},[389,1076,989],{"class":428},[389,1078,992],{"class":405},[389,1080,1081],{"class":588},"'layout-shift'",[389,1083,1004],{"class":405},[389,1085,1007],{"class":441},[389,1087,1010],{"class":405},[389,1089,1091],{"class":167,"line":1090},20,[389,1092,460],{"emptyLinePlaceholder":459},[389,1094,1096,1099,1101,1104,1106,1108],{"class":167,"line":1095},21,[389,1097,1098],{"class":428},"onSoftNavigation",[389,1100,604],{"class":405},[389,1102,1103],{"class":478},"prev",[389,1105,609],{"class":405},[389,1107,612],{"class":401},[389,1109,904],{"class":405},[389,1111,1113,1116,1119,1122],{"class":167,"line":1112},22,[389,1114,1115],{"class":428},"  send",[389,1117,1118],{"class":405},"({ route: prev.route, inp: Math.",[389,1120,1121],{"class":428},"round",[389,1123,1124],{"class":405},"(route.inp), interactions: route.interactions,\n",[389,1126,1128,1131,1134,1137,1140,1142,1144],{"class":167,"line":1127},23,[389,1129,1130],{"class":405},"         cls: ",[389,1132,1133],{"class":401},"+",[389,1135,1136],{"class":405},"route.shifts.",[389,1138,1139],{"class":428},"toFixed",[389,1141,475],{"class":405},[389,1143,118],{"class":441},[389,1145,1146],{"class":405},") });\n",[389,1148,1150,1153,1155,1157,1159,1161,1163,1165,1167],{"class":167,"line":1149},24,[389,1151,1152],{"class":405},"  route ",[389,1154,409],{"class":401},[389,1156,855],{"class":405},[389,1158,83],{"class":441},[389,1160,860],{"class":405},[389,1162,83],{"class":441},[389,1164,865],{"class":405},[389,1166,83],{"class":441},[389,1168,870],{"class":405},[389,1170,1172],{"class":167,"line":1171},25,[389,1173,1174],{"class":405},"});\n",[389,1176,1178],{"class":167,"line":1177},26,[389,1179,1180],{"class":394},"\u002F\u002F trade-off: summing shifts per route overstates CLS on long-lived routes,\n",[389,1182,1184],{"class":167,"line":1183},27,[389,1185,1186],{"class":394},"\u002F\u002F because the official metric keeps only the worst session window. Use the\n",[389,1188,1190],{"class":167,"line":1189},28,[389,1191,1192],{"class":394},"\u002F\u002F window-splitting approach in the CLS guide when the route stays open for minutes.\n",[15,1194,1195,1196,1199],{},"Note that the \"worst interaction per route\" here diverges slightly from the official INP definition, which ignores one outlier for every 50 interactions. For routes with few interactions — the common case — the two are identical; track ",[37,1197,1198],{},"interactions"," so you know when they are not.",[208,1201,1203],{"id":1202},"deconstructing-an-spa-route-change","Deconstructing an SPA Route Change",[15,1205,1206,1207,1210],{},"A soft navigation has its own phase model, analogous to a page load but without DNS, TLS or document parsing. Budget each phase so the whole transition — click to new content painted — stays under roughly ",[37,1208,1209],{},"1s",", the point at which users perceive a navigation rather than a reaction.",[1212,1213,1214,1236],"table",{},[1215,1216,1217],"thead",{},[1218,1219,1220,1224,1227,1230,1233],"tr",{},[1221,1222,1223],"th",{},"Phase",[1221,1225,1226],{},"Starts",[1221,1228,1229],{},"Ends",[1221,1231,1232],{},"Budget",[1221,1234,1235],{},"Typical bottleneck",[1237,1238,1239,1257,1274,1291],"tbody",{},[1218,1240,1241,1245,1248,1251,1254],{},[1242,1243,1244],"td",{},"Interaction",[1242,1246,1247],{},"pointer\u002Fkeyboard input",[1242,1249,1250],{},"router handler starts",[1242,1252,1253],{},"\u003C 50ms",[1242,1255,1256],{},"long task in progress",[1218,1258,1259,1262,1265,1268,1271],{},[1242,1260,1261],{},"Route code",[1242,1263,1264],{},"router handler",[1242,1266,1267],{},"route chunk evaluated",[1242,1269,1270],{},"\u003C 200ms",[1242,1272,1273],{},"un-prefetched chunk download",[1218,1275,1276,1279,1282,1285,1288],{},[1242,1277,1278],{},"Data",[1242,1280,1281],{},"chunk evaluated",[1242,1283,1284],{},"data resolved",[1242,1286,1287],{},"\u003C 400ms",[1242,1289,1290],{},"serial API waterfall",[1218,1292,1293,1296,1298,1301,1303],{},[1242,1294,1295],{},"Render",[1242,1297,1284],{},[1242,1299,1300],{},"new content painted",[1242,1302,1270],{},[1242,1304,1305],{},"large component tree commit",[15,1307,1308],{},[70,1309,77,1312,77,1315,77,1318,77,1320,77,1323,77,1325,77,1331,77,1334,77,1338,77,1342,77,1347,77,1351,77,1355,77,1359,77,1363,77,1366,77,1371,77,1374,77,1378,77,1382,77,1386,77,1389,77,1393,77],{"viewBox":1310,"width":73,"role":74,"ariaLabel":1311,"style":76},"0 0 760 162","Timeline of a soft navigation split into interaction, route code, data and render phases, with the data phase dominating.",[79,1313],{"className":1314,"x":83,"y":83,"width":73,"height":73,"fill":84},[82],[86,1316,1317],{},"Phases of a 1.1 second soft navigation",[90,1319,1311],{},[79,1321],{"x":94,"y":94,"width":95,"height":1322,"rx":97,"fill":98,"stroke":99,"style":100},"160",[102,1324,1317],{"x":104,"y":105,"fill":99,"style":106},[167,1326],{"x1":160,"y1":1327,"x2":160,"y2":293,"stroke":132,"strokeWidth":746,"strokeDashArray":1328,"style":1330},"72.0",[1329,1329],"4","stroke-opacity:0.9",[102,1332,1333],{"x":104,"y":705,"fill":99,"style":110},"Phases",[79,1335],{"x":1336,"y":700,"width":1337,"height":117,"rx":118,"fill":99,"stroke":99,"style":141},"87.5","19.8",[79,1339],{"x":1340,"y":700,"width":1341,"height":117,"rx":118,"fill":119,"stroke":119,"style":120},"108.8","131.7",[102,1343,1346],{"x":1344,"y":1345,"fill":99,"style":125,"textAnchor":126},"174.7","95.5","route chunk",[79,1348],{"x":1349,"y":700,"width":1350,"height":117,"rx":118,"fill":131,"stroke":132,"style":133},"242.0","302.1",[102,1352,1354],{"x":1353,"y":1345,"fill":99,"style":125,"textAnchor":126},"393.1","data (2 serial calls)",[79,1356],{"x":1357,"y":700,"width":1358,"height":117,"rx":118,"fill":119,"stroke":119,"style":120},"545.6","126.3",[102,1360,1362],{"x":1361,"y":1345,"fill":99,"style":125,"textAnchor":126},"608.8","render",[167,1364],{"x1":1365,"y1":293,"x2":171,"y2":293,"stroke":99,"style":172},"86.8",[102,1367,1370],{"x":1368,"y":1369,"fill":177,"style":125,"textAnchor":126},"97.5","135.0","0ms",[102,1372,61],{"x":1373,"y":1369,"fill":177,"style":125,"textAnchor":126},"193.3",[102,1375,1377],{"x":1376,"y":1369,"fill":177,"style":125,"textAnchor":126},"299.9","400ms",[102,1379,1381],{"x":1380,"y":1369,"fill":177,"style":125,"textAnchor":126},"406.4","600ms",[102,1383,1385],{"x":1384,"y":1369,"fill":177,"style":125,"textAnchor":126},"512.9","800ms",[102,1387,1388],{"x":160,"y":1369,"fill":177,"style":125,"textAnchor":126},"1000ms",[102,1390,1392],{"x":1391,"y":1369,"fill":177,"style":125,"textAnchor":126},"712.3","1200ms",[102,1394,1398],{"x":160,"y":1395,"fill":1396,"style":1397,"textAnchor":126},"68.0","#8a6608","font-size:12px;font-weight:700","1s transition budget",[15,1400,1401],{},"The interaction phase is also the INP interaction for the click, so a slow router handler fails INP and transition time at once. The data phase is usually the biggest and the easiest to parallelise: start the route's data request in the same tick as the chunk request, not after the component mounts.",[208,1403,1405],{"id":1404},"advanced-diagnostics-and-edge-cases","Advanced Diagnostics and Edge Cases",[15,1407,1408,1411,1412,1415],{},[218,1409,1410],{},"Navigations without interactions."," A redirect after login, an auto-advancing wizard, or a ",[37,1413,1414],{},"router.replace"," to normalise a URL all change the route without user input. Counting them inflates view counts and creates routes with zero interactions and misleading LCP. Gate on a recent interaction (within, say, 1 second) unless the navigation is a deliberate user-visible view.",[15,1417,1418,77,1421,1424,1425,32],{},[218,1419,1420],{},"Back and forward in SPAs.",[37,1422,1423],{},"popstate"," navigations restore a route whose data may be cached. Their transitions are fast and will flatter your averages; tag them so you can analyse them separately, and do not confuse them with bfcache restores of the whole document, covered in the ",[19,1426,1428],{"href":1427},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002F","back\u002Fforward cache topic",[15,1430,1431,1434,1435,1438],{},[218,1432,1433],{},"Overlapping navigations."," Users click again before the previous route has painted. Finalise the abandoned route with an ",[37,1436,1437],{},"aborted: true"," flag rather than letting its metrics leak into the next one.",[15,1440,1441,1444],{},[218,1442,1443],{},"Hydration on the landing route."," The first route is a hard navigation and carries hydration cost the others do not. Comparing landing-route INP with in-app routes without accounting for this makes in-app routes look better than they are relative to their own code.",[15,1446,1447,1450,1451,1455,1456,1459],{},[218,1448,1449],{},"Prerendered routes."," With ",[19,1452,1454],{"href":1453},"\u002Fnetwork-protocol-optimization\u002Fspeculative-loading-and-prefetching\u002Fspeculation-rules-for-instant-navigations\u002F","speculation rules",", a \"navigation\" may activate a prerendered document — a hard navigation that looks instantaneous. Use ",[37,1457,1458],{},"activationStart"," to normalise its timestamps before comparing with soft navigations.",[208,1461,1463],{"id":1462},"validation-and-budgeting","Validation and Budgeting",[15,1465,1466,1467,1470,1471,1473,1474,1476],{},"Validate per-route attribution by deliberately breaking one route in a staging build — add a ",[37,1468,1469],{},"150ms"," busy loop to a handler on ",[37,1472,47],{}," — and confirm that only ",[37,1475,47],{}," regresses in your RUM, not the landing route. Then set budgets per route template rather than per URL:",[380,1478,1482],{"className":1479,"code":1480,"language":1481,"meta":385,"style":385},"language-yaml shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# perf-budgets.yml — evaluated nightly against the last 7 days of RUM.\nroutes:\n  \u002Fproducts:        { lcp_p75: 2500, inp_p75: 200, cls_p75: 0.1 }\n  \u002Fproducts\u002F:id:    { route_lcp_p75: 1500, inp_p75: 200, transition_p75: 1000 }\n  \u002Fcart:            { inp_p75: 150, transition_p75: 800 }\n# trade-off: route-level budgets need enough traffic per template for a stable\n# p75 — roughly 500+ samples a week. Merge low-traffic routes into one bucket\n# rather than alerting on noise.\n","yaml",[37,1483,1484,1489,1498,1538,1574,1600,1605,1610],{"__ignoreMap":385},[389,1485,1486],{"class":167,"line":391},[389,1487,1488],{"class":394},"# perf-budgets.yml — evaluated nightly against the last 7 days of RUM.\n",[389,1490,1491,1495],{"class":167,"line":398},[389,1492,1494],{"class":1493},"sZBmE","routes",[389,1496,1497],{"class":405},":\n",[389,1499,1500,1503,1506,1509,1512,1515,1518,1521,1523,1526,1528,1531,1533,1535],{"class":167,"line":415},[389,1501,1502],{"class":1493},"  \u002Fproducts",[389,1504,1505],{"class":405},":        { ",[389,1507,1508],{"class":1493},"lcp_p75",[389,1510,1511],{"class":405},": ",[389,1513,1514],{"class":441},"2500",[389,1516,1517],{"class":405},", ",[389,1519,1520],{"class":1493},"inp_p75",[389,1522,1511],{"class":405},[389,1524,1525],{"class":441},"200",[389,1527,1517],{"class":405},[389,1529,1530],{"class":1493},"cls_p75",[389,1532,1511],{"class":405},[389,1534,65],{"class":441},[389,1536,1537],{"class":405}," }\n",[389,1539,1540,1543,1546,1549,1551,1554,1556,1558,1560,1562,1564,1567,1569,1572],{"class":167,"line":435},[389,1541,1542],{"class":1493},"  \u002Fproducts\u002F:id",[389,1544,1545],{"class":405},":    { ",[389,1547,1548],{"class":1493},"route_lcp_p75",[389,1550,1511],{"class":405},[389,1552,1553],{"class":441},"1500",[389,1555,1517],{"class":405},[389,1557,1520],{"class":1493},[389,1559,1511],{"class":405},[389,1561,1525],{"class":441},[389,1563,1517],{"class":405},[389,1565,1566],{"class":1493},"transition_p75",[389,1568,1511],{"class":405},[389,1570,1571],{"class":441},"1000",[389,1573,1537],{"class":405},[389,1575,1576,1579,1582,1584,1586,1589,1591,1593,1595,1598],{"class":167,"line":456},[389,1577,1578],{"class":1493},"  \u002Fcart",[389,1580,1581],{"class":405},":            { ",[389,1583,1520],{"class":1493},[389,1585,1511],{"class":405},[389,1587,1588],{"class":441},"150",[389,1590,1517],{"class":405},[389,1592,1566],{"class":1493},[389,1594,1511],{"class":405},[389,1596,1597],{"class":441},"800",[389,1599,1537],{"class":405},[389,1601,1602],{"class":167,"line":463},[389,1603,1604],{"class":394},"# trade-off: route-level budgets need enough traffic per template for a stable\n",[389,1606,1607],{"class":167,"line":491},[389,1608,1609],{"class":394},"# p75 — roughly 500+ samples a week. Merge low-traffic routes into one bucket\n",[389,1611,1612],{"class":167,"line":496},[389,1613,1614],{"class":394},"# rather than alerting on noise.\n",[15,1616,1617],{},[70,1618,77,1621,77,1624,77,1627,77,1629,77,1632,77,1634,77,1638,77,1644,77,1648,77,1652,77,1655,77,1659,77,1662,77,1666,77,1670,77,1673,77,1675,77,1678,77,1680,77,1683,77,1685,77,1688,77,1691,77,1694,77,1696,77,1699,77,1701,77,1703,77,1705,77,1708,77,1710,77,1713,77,1715,77,1718,77,1720,77,1722,77,1724,77,1727,77,1730,77,1734,77,1736,77,1739,77,1741,77,1744,77,1746,77],{"viewBox":1619,"width":73,"role":74,"ariaLabel":1620,"style":76},"0 0 760 228","For each Core Web Vital, whether the browser measures it on soft navigations, how it is attributed by default, and what to change.",[79,1622],{"className":1623,"x":83,"y":83,"width":73,"height":73,"fill":84},[82],[86,1625,1626],{},"Which metric needs which fix in an SPA",[90,1628,1620],{},[79,1630],{"x":94,"y":94,"width":95,"height":1631,"rx":97,"fill":98,"stroke":99,"style":100},"226",[102,1633,1626],{"x":104,"y":105,"fill":99,"style":106},[79,1635],{"x":104,"y":115,"width":1636,"height":1637,"rx":83,"fill":99,"stroke":99,"style":141},"108.5","30.0",[102,1639,1643],{"x":1640,"y":1641,"fill":99,"style":110,"textAnchor":1642},"38.0","75.5","start","Metric",[79,1645],{"x":1646,"y":115,"width":1647,"height":1637,"rx":83,"fill":99,"stroke":99,"style":141},"136.5","198.5",[102,1649,1651],{"x":1650,"y":1641,"fill":99,"style":110,"textAnchor":126},"235.7","Measured on soft nav?",[79,1653],{"x":1654,"y":115,"width":1647,"height":1637,"rx":83,"fill":99,"stroke":99,"style":141},"335.0",[102,1656,1658],{"x":1657,"y":1641,"fill":99,"style":110,"textAnchor":126},"434.2","Default attribution",[79,1660],{"x":1661,"y":115,"width":1647,"height":1637,"rx":83,"fill":99,"stroke":99,"style":141},"533.5",[102,1663,1665],{"x":1664,"y":1641,"fill":99,"style":110,"textAnchor":126},"632.7","Your fix",[79,1667],{"x":104,"y":1668,"width":1636,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},"86.0","stroke-opacity:0.35",[102,1671,148],{"x":1640,"y":1672,"fill":99,"style":110,"textAnchor":1642},"105.5",[79,1674],{"x":1646,"y":1668,"width":1647,"height":1637,"rx":83,"fill":131,"stroke":132,"style":133},[102,1676,1677],{"x":1650,"y":1672,"fill":99,"style":706,"textAnchor":126},"No (experimental only)",[79,1679],{"x":1654,"y":1668,"width":1647,"height":1637,"rx":83,"fill":99,"stroke":99,"style":141},[102,1681,1682],{"x":1657,"y":1672,"fill":99,"style":706,"textAnchor":126},"Landing URL",[79,1684],{"x":1661,"y":1668,"width":1647,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1686,1687],{"x":1664,"y":1672,"fill":99,"style":706,"textAnchor":126},"Route contentful paint",[79,1689],{"x":104,"y":1690,"width":1636,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},"116.0",[102,1692,779],{"x":1640,"y":1693,"fill":99,"style":110,"textAnchor":1642},"135.5",[79,1695],{"x":1646,"y":1690,"width":1647,"height":1637,"rx":83,"fill":119,"stroke":119,"style":120},[102,1697,1698],{"x":1650,"y":1693,"fill":99,"style":706,"textAnchor":126},"Yes",[79,1700],{"x":1654,"y":1690,"width":1647,"height":1637,"rx":83,"fill":131,"stroke":132,"style":133},[102,1702,1682],{"x":1657,"y":1693,"fill":99,"style":706,"textAnchor":126},[79,1704],{"x":1661,"y":1690,"width":1647,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1706,1707],{"x":1664,"y":1693,"fill":99,"style":706,"textAnchor":126},"Tag interactions with route",[79,1709],{"x":104,"y":695,"width":1636,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1711,789],{"x":1640,"y":1712,"fill":99,"style":110,"textAnchor":1642},"165.5",[79,1714],{"x":1646,"y":695,"width":1647,"height":1637,"rx":83,"fill":119,"stroke":119,"style":120},[102,1716,1717],{"x":1650,"y":1712,"fill":99,"style":706,"textAnchor":126},"Yes, accumulated",[79,1719],{"x":1654,"y":695,"width":1647,"height":1637,"rx":83,"fill":131,"stroke":132,"style":133},[102,1721,1682],{"x":1657,"y":1712,"fill":99,"style":706,"textAnchor":126},[79,1723],{"x":1661,"y":695,"width":1647,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1725,1726],{"x":1664,"y":1712,"fill":99,"style":706,"textAnchor":126},"Split windows on navigation",[79,1728],{"x":104,"y":1729,"width":1636,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},"176.0",[102,1731,1733],{"x":1640,"y":1732,"fill":99,"style":110,"textAnchor":1642},"195.5","Transition time",[79,1735],{"x":1646,"y":1729,"width":1647,"height":1637,"rx":83,"fill":131,"stroke":132,"style":133},[102,1737,1738],{"x":1650,"y":1732,"fill":99,"style":706,"textAnchor":126},"Not a standard metric",[79,1740],{"x":1654,"y":1729,"width":1647,"height":1637,"rx":83,"fill":99,"stroke":99,"style":141},[102,1742,1743],{"x":1657,"y":1732,"fill":99,"style":706,"textAnchor":126},"n\u002Fa",[79,1745],{"x":1661,"y":1729,"width":1647,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1747,1748],{"x":1664,"y":1732,"fill":99,"style":706,"textAnchor":126},"User Timing marks",[208,1750,1752],{"id":1751},"router-integration-notes-by-framework","Router Integration Notes by Framework",[15,1754,1755],{},"The boundary hook you choose determines what your per-route numbers mean, and each router exposes a slightly different set.",[15,1757,1758,77,1761,1764,1765,1768,1769,1772,1773,1775,1776,1779,1780,1783,1784,1787,1788,1791,1792,1795,1796,1798],{},[218,1759,1760],{},"Vue Router (Vue, Nuxt).",[37,1762,1763],{},"router.beforeEach"," fires when the navigation is requested, ",[37,1766,1767],{},"beforeResolve"," after async route components and in-component guards resolve, and ",[37,1770,1771],{},"afterEach"," once the navigation is confirmed — but before the DOM has updated. For \"route committed\" use ",[37,1774,1771],{}," plus ",[37,1777,1778],{},"nextTick()","; for \"route painted\" add a ",[37,1781,1782],{},"requestAnimationFrame"," after that. Nuxt adds ",[37,1785,1786],{},"page:start"," and ",[37,1789,1790],{},"page:finish"," hooks that already account for ",[37,1793,1794],{},"\u003CSuspense>"," resolution, which makes ",[37,1797,1790],{}," the cleaner end marker.",[15,1800,1801,1804,1805,1808,1809,1812,1813,1816,1817,1819,1820,1823,1824,32],{},[218,1802,1803],{},"React Router (v6.4+ data routers)."," Loaders run before the route renders, so ",[37,1806,1807],{},"useNavigation().state"," moving from ",[37,1810,1811],{},"loading"," to ",[37,1814,1815],{},"idle"," marks data readiness. For the start, capture the click in a capture-phase listener; the router has no \"navigation requested\" callback that precedes loader execution in every mode. A ",[37,1818,320],{}," keyed on ",[37,1821,1822],{},"location.key"," runs after commit and is the right place for ",[37,1825,648],{},[15,1827,1828,1831,1832,1787,1835,1838],{},[218,1829,1830],{},"Next.js App Router."," There is no public router event API in the App Router. Use ",[37,1833,1834],{},"usePathname()",[37,1836,1837],{},"useSearchParams()"," in a client component mounted in the root layout; its effect runs after the new segment commits. Server components streaming in after that point will not be captured by the effect, so pair it with Element Timing on the segment's hero for an end marker.",[15,1840,1841,77,1844,1787,1847,1849,1850,1853,1854,1517,1857,1517,1859,1787,1862,1865,1866,1787,1868,1870],{},[218,1842,1843],{},"SvelteKit.",[37,1845,1846],{},"beforeNavigate",[37,1848,328],{}," bracket the navigation cleanly, and ",[37,1851,1852],{},"navigation.type"," distinguishes ",[37,1855,1856],{},"link",[37,1858,1423],{},[37,1860,1861],{},"goto",[37,1863,1864],{},"form",". Filter on ",[37,1867,1856],{},[37,1869,1864],{}," to count only user-initiated views.",[15,1872,1873,1876,1877,1880],{},[218,1874,1875],{},"The Navigation API."," Where supported, ",[37,1878,1879],{},"navigation.addEventListener('navigatesuccess', …)"," fires after the intercepted navigation's handler promise resolves, which gives you a framework-independent end point. It is the most promising long-term primitive, but coverage is still incomplete across engines, so keep the router-specific path as the default.",[15,1882,1883],{},"Whichever hook you use, test the boundary by navigating rapidly between three routes: if any metric is attributed to the wrong route in that test, the hook fires too early or too late.",[15,1885,1886],{},[70,1887,77,1890,77,1893,77,1896,77,1898,77,1901,77,1903,77,1906,77,1909,77,1913,77,1917,77,1920,77,1924,77,1926,77,1929,77,1931,77,1934,77,1936,77,1939,77,1941,77,1944,77,1946,77,1949,77,1951,77,1954,77,1956,77,1959,77,1961,77,1963,77,1965,77,1968,77,1970,77,1973,77,1975,77,1977,77,1979,77,1981,77,1984,77,1988,77,1990,77,1993,77,1995,77],{"viewBox":1888,"width":73,"role":74,"ariaLabel":1889,"style":76},"0 0 760 258","For each router, the hook to use for the start of a soft navigation and the hook to use for the committed view.",[79,1891],{"className":1892,"x":83,"y":83,"width":73,"height":73,"fill":84},[82],[86,1894,1895],{},"Navigation boundary hooks by router",[90,1897,1889],{},[79,1899],{"x":94,"y":94,"width":95,"height":1900,"rx":97,"fill":98,"stroke":99,"style":100},"256",[102,1902,1895],{"x":104,"y":105,"fill":99,"style":106},[79,1904],{"x":104,"y":115,"width":1905,"height":1637,"rx":83,"fill":99,"stroke":99,"style":141},"129.4",[102,1907,1908],{"x":1640,"y":1641,"fill":99,"style":110,"textAnchor":1642},"Router",[79,1910],{"x":1911,"y":115,"width":1912,"height":1637,"rx":83,"fill":99,"stroke":99,"style":141},"157.4","287.3",[102,1914,1916],{"x":1915,"y":1641,"fill":99,"style":110,"textAnchor":126},"301.0","Start marker",[79,1918],{"x":1919,"y":115,"width":1912,"height":1637,"rx":83,"fill":99,"stroke":99,"style":141},"444.7",[102,1921,1923],{"x":1922,"y":1641,"fill":99,"style":110,"textAnchor":126},"588.3","Committed marker",[79,1925],{"x":104,"y":1668,"width":1905,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1927,1928],{"x":1640,"y":1672,"fill":99,"style":110,"textAnchor":1642},"Vue Router \u002F Nuxt",[79,1930],{"x":1911,"y":1668,"width":1912,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1932,1933],{"x":1915,"y":1672,"fill":99,"style":706,"textAnchor":126},"beforeEach",[79,1935],{"x":1919,"y":1668,"width":1912,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1937,1938],{"x":1922,"y":1672,"fill":99,"style":706,"textAnchor":126},"page:finish or afterEach + nextTick",[79,1940],{"x":104,"y":1690,"width":1905,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1942,1943],{"x":1640,"y":1693,"fill":99,"style":110,"textAnchor":1642},"React Router",[79,1945],{"x":1911,"y":1690,"width":1912,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1947,1948],{"x":1915,"y":1693,"fill":99,"style":706,"textAnchor":126},"capture-phase click",[79,1950],{"x":1919,"y":1690,"width":1912,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1952,1953],{"x":1922,"y":1693,"fill":99,"style":706,"textAnchor":126},"useEffect on location.key",[79,1955],{"x":104,"y":695,"width":1905,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1957,1958],{"x":1640,"y":1712,"fill":99,"style":110,"textAnchor":1642},"Next.js App Router",[79,1960],{"x":1911,"y":695,"width":1912,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1962,1948],{"x":1915,"y":1712,"fill":99,"style":706,"textAnchor":126},[79,1964],{"x":1919,"y":695,"width":1912,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1966,1967],{"x":1922,"y":1712,"fill":99,"style":706,"textAnchor":126},"root layout effect on pathname",[79,1969],{"x":104,"y":1729,"width":1905,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1971,1972],{"x":1640,"y":1732,"fill":99,"style":110,"textAnchor":1642},"SvelteKit",[79,1974],{"x":1911,"y":1729,"width":1912,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1976,1846],{"x":1915,"y":1732,"fill":99,"style":706,"textAnchor":126},[79,1978],{"x":1919,"y":1729,"width":1912,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1980,328],{"x":1922,"y":1732,"fill":99,"style":706,"textAnchor":126},[79,1982],{"x":104,"y":1983,"width":1905,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},"206.0",[102,1985,1987],{"x":1640,"y":1986,"fill":99,"style":110,"textAnchor":1642},"225.5","Navigation API",[79,1989],{"x":1911,"y":1983,"width":1912,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1991,1992],{"x":1915,"y":1986,"fill":99,"style":706,"textAnchor":126},"navigate event",[79,1994],{"x":1919,"y":1983,"width":1912,"height":1637,"rx":83,"fill":98,"stroke":99,"style":1669},[102,1996,332],{"x":1922,"y":1986,"fill":99,"style":706,"textAnchor":126},[208,1998,2000],{"id":1999},"faq","FAQ",[2002,2003,2006,2010],"details",{"className":2004},[2005],"faq-item",[2007,2008,2009],"summary",{},"Will CrUX ever report soft navigations separately?",[15,2011,2012],{},"That depends on the Soft Navigations API leaving its experimental phase and on Chrome deciding to aggregate it, neither of which is settled. Plan as if CrUX will continue to attribute everything to the landing URL for the foreseeable future. Your own RUM is the only place per-route numbers live today, which is a strong reason to instrument it rather than wait.",[2002,2014,2016,2019],{"className":2015},[2005],[2007,2017,2018],{},"Should I convert my SPA to multi-page navigation to fix the metrics?",[15,2020,2021,2022,2026],{},"Not to fix the metrics — that would be optimising the measurement rather than the experience. There are real performance arguments for multi-page architectures with ",[19,2023,2025],{"href":2024},"\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fview-transitions-api-performance\u002F","view transitions"," and prerendering, but they should be judged on user-facing speed. Measuring soft navigations properly is a prerequisite for making that comparison honestly.",[2002,2028,2030,2033],{"className":2029},[2005],[2007,2031,2032],{},"Does Google Search use per-route data for ranking?",[15,2034,2035],{},"Search uses CrUX, which is per document URL. Pages that are only reached via soft navigation and never loaded directly have little or no data of their own. That is another reason landing-page experience matters disproportionately: it is the only part of an SPA session that field data assigns to the right URL.",[2002,2037,2039,2042],{"className":2038},[2005],[2007,2040,2041],{},"How many soft navigations per session are typical, and does it matter?",[15,2043,2044],{},"On content sites, sessions average one to three views; on dashboards and storefronts it is common to see ten or more in-app routes per session. It matters because every additional route adds interactions to the INP pool and layout-shift windows to the CLS pool, while LCP stays fixed to the landing view. The more routes per session, the further the official numbers drift from what any single view is like, and the more value per-route measurement adds. Record views-per-session alongside your metrics so the drift is visible rather than assumed.",[208,2046,2048],{"id":2047},"guides-in-this-topic","Guides in This Topic",[305,2050,2051,2057,2063,2068],{},[308,2052,2053,2056],{},[19,2054,2055],{"href":773},"Measuring LCP after client-side route changes"," — a route-level contentful paint you can trust.",[308,2058,2059,2062],{},[19,2060,2061],{"href":783},"Attributing INP to the right route in an SPA"," — stop charging the cart's interactions to the homepage.",[308,2064,2065,2067],{},[19,2066,804],{"href":803}," — click-to-content as a measured, budgeted number.",[308,2069,2070,2073],{},[19,2071,2072],{"href":793},"Why SPA CLS accumulates across routes"," — splitting session windows at navigation boundaries.",[208,2075,2077],{"id":2076},"related","Related",[305,2079,2080,2087,2094,2101],{},[308,2081,2082,2086],{},[19,2083,2085],{"href":2084},"\u002Fcore-web-vitals-measurement\u002Foptimizing-first-input-delay-fid\u002Fimproving-inp-for-complex-single-page-applications\u002F","Improving INP for complex single page applications"," — the optimisation side of the same applications.",[308,2088,2089,2093],{},[19,2090,2092],{"href":2091},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002F","Long Animation Frames (LoAF) API"," — script attribution that pairs naturally with route attribution.",[308,2095,2096,2100],{},[19,2097,2099],{"href":2098},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fcode-splitting-vue-router-routes\u002F","Code-splitting Vue Router routes"," — shrinking the route-code phase of every transition.",[308,2102,2103,2107],{},[19,2104,2106],{"href":2105},"\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: why the numbers differ"," — reconciling per-route data with the public dataset.",[2109,2110,2112],"script",{"type":2111},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Soft Navigations and SPA Metrics: Measuring Core Web Vitals Per Route\",\n  \"description\": \"Why client-side route changes break Core Web Vitals attribution, and how to measure each route of a single-page app on its own terms.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Environment Setup: Define What Counts as a Soft Navigation\",\n      \"text\": \"Before measuring anything, agree on a definition.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Capture a Baseline per Route\",\n      \"text\": \"Before changing anything, split your existing RUM by the route active at the time each metric was finalised.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Isolate the Bottleneck: Which Metric, Which Route\",\n      \"text\": \"Treat each metric separately because each has a different measurement gap:\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Apply the Fix: Per-Route Metric Reporting\",\n      \"text\": \"The core change is to finalise and send each metric at every soft-navigation boundary, then start fresh for the next route.\"\n    }\n  ]\n}\n",[2109,2114,2115],{"type":2111},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"Soft Navigations and SPA Metrics: Measuring Core Web Vitals Per Route\",\n  \"description\": \"Why client-side route changes break Core Web Vitals attribution, and how to measure each route of a single-page app on its own terms.\",\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\u002F\"\n  }\n}\n",[2109,2117,2118],{"type":2111},"\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}\n",[2120,2121,2122],"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 .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 .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}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 .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--shiki-light:#024C1A}",{"title":385,"searchDepth":398,"depth":398,"links":2124},[2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2137],{"id":210,"depth":398,"text":211},{"id":302,"depth":398,"text":303},{"id":374,"depth":398,"text":375},{"id":652,"depth":398,"text":653},{"id":759,"depth":398,"text":760},{"id":808,"depth":398,"text":809},{"id":1202,"depth":398,"text":1203},{"id":1404,"depth":398,"text":1405},{"id":1462,"depth":398,"text":1463},{"id":1751,"depth":398,"text":1752},{"id":1999,"depth":398,"text":2000},{"id":2047,"depth":398,"text":2048},{"id":2076,"depth":398,"text":2077},"Why client-side route changes break Core Web Vitals attribution, and how to measure each route of a single-page app on its own terms.","md",{"slug":2141,"type":2142,"breadcrumb":2143,"datePublished":2149,"dateModified":2149},"soft-navigations-and-spa-metrics","topic",[2144,2146,2147],{"name":2145,"url":325},"Home",{"name":22,"url":21},{"name":5,"url":2148},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002F","2026-10-06","\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics",{"title":2152,"description":2153},"Soft Navigations & SPA Core Web Vitals Measurement","Measure LCP, INP and CLS correctly in single-page apps: why route changes are invisible to Core Web Vitals, and how to attribute metrics per route.","core-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Findex","Bj5A3jIU3TBU4yNgKaRfr8p7Bb8z_Ag-_m8LKc76Ot4",[2157,2161],{"title":2158,"path":2159,"stem":2160,"children":-1},"Sending Web Vitals with sendBeacon on visibilitychange","\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change","core-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsending-web-vitals-with-sendbeacon-on-visibility-change\u002Findex",{"title":2162,"path":2163,"stem":2164,"children":-1},"Attributing INP to the Right Route in an SPA","\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa","core-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fattributing-inp-to-the-right-route-in-an-spa\u002Findex",1791308070241]