Zum Inhalt springen
Core Web Vitals specialists
Core Web Vitals

Soft Navigations: Measuring Core Web Vitals in SPAs

Single-page apps only report the first load to CrUX. How the Soft Navigations API from Chrome 151 measures route changes and splits LCP, INP and CLS per route.

14 min read Core Web VitalsSoft NavigationsSPAINPMessung

A category page loads, the user clicks a product, filters, adds to the cart and moves to checkout: five pages by feel, one single document load technically. That is exactly where measurement stops. Core Web Vitals are measured relative to the current top-level page navigation, and a route change via the History API has no effect on how the metrics are measured - the values are not reset (Source: web.dev). LCP therefore measures the first render and never again, while CLS and INP keep running across the entire page lifecycle (Source: web.dev). On top of that, CrUX aggregates only by origin or by the page URL at load time and cannot group by SPA route (Source: web.dev). Many single-page app operators know the result: passable field data alongside a bad gut feeling. Chrome closes this gap with the Soft Navigations API, available unflagged for all sites from Chrome 151 (Source: Chrome for Developers). This article shows how the detection works, which entry types it delivers, and how LCP, INP and CLS can be cut into route segments with it - the basis of any solid Core Web Vitals optimization.

One SPA session: what gets measured and what is missingWithout soft navCrUX today1 entry for the whole session — LCP only at document loadCLS and INP accumulate across the entire page lifecycleRoutes: invisibleHeuristic1 · user interaction2 · history change3 · visible paintWith soft-navigationfrom Chrome 151/categoryHard navigationnavigationId · real TTFB/product/a12Soft nav 1navigationId · TTFB 0/cartSoft nav 2navigationId · TTFB 0/checkoutSoft nav 3navigationId · TTFB 0LCPmeasured again per route,via getLargestInteractionContentfulPaint()CLSrestarts at every routeboundary at 0INPper route instead of acrossthe whole page lifecycle151Chrome version withunflagged API7entry types receivea navigationId48 %of mobile websites withgood Core Web VitalsSources: Chrome for Developers · HTTP Archive Web Almanac 2025

Why SPAs only report the first load

The measurement gap is not a bug but a consequence of the definition. All Core Web Vitals metrics relate to the current top-level page navigation (Source: web.dev). A hard navigation - an actual document load - resets that reference point. A route change in a single-page app does not: the browser swaps DOM content and changes the URL via pushState or replaceState, but the document stays the same. For the measurement layer, the very first load therefore remains authoritative.

Each metric has its own consequence. LCP belongs to the page load that initiated the navigation - when a user clicks from a category into a product detail page, no new LCP candidate is created even though the entire visible content is replaced. CLS has only accumulated over the worst burst window since April 2021, but it is not reset after a route transition the way it is on full page loads in a classic multi-page architecture (Source: web.dev). INP measures interaction latency across the entire page lifecycle (Source: web.dev) - in a long-lived SPA that means minutes and dozens of routes condensed into one single value.

Then there is the aggregation level. CrUX groups data by origin or by page URL, where the URL at load time applies - grouping by SPA route is therefore impossible (Source: web.dev). Everything a user experiences after entry is attributed to the entry URL. Anyone wanting to know whether checkout is slower than the category page will simply not find the answer in the field data. Sound technical analysis must address this gap actively rather than skip over it.

The direction of the distortion is interesting: it is not consistently flattering. The one-off LCP can produce a good value while the actual route performance is never checked - that flatters. INP and CLS, by contrast, collect across the whole session and are dominated by the worst moment - that penalises. Google also notes that there is nothing inherent in the SPA architecture that would prevent a page from loading just as quickly; well-optimised multi-page architectures do benefit from subsequent loads coming from cache and making up a larger share of the distribution at the 75th percentile (Source: web.dev). How CrUX and your own monitoring diverge in general is covered in the article on field data from CrUX and your own RUM.

Scope: the measurement gap, not framework tuning

This article is about measurement, not about tuning the framework. Whether hydration, streaming or an island architecture is the better choice is a separate question - the article on server-side rendering and hydration sorts through those build styles. Here the sole aim is making the real user journey visible at all. Without that visibility, any framework tuning optimises against a gut feeling.

How Chrome detects a soft navigation

Chrome does not detect a soft navigation through a framework hook but heuristically, from the observable behaviour of the page. Three conditions must come together: the navigation is initiated by a user action, it results in a visible URL change for the user, and the interaction results in a visible paint (Source: Chrome for Developers). Only when all three are met does the process count as a navigation.

The specification puts the same thing more technically: a soft navigation is a user-initiated same-document navigation made up of a trusted interaction, a dynamic DOM update and a same-document history change via the History API or the Navigation API (Source: WICG Soft Navigations). At least some contentful update to the page must be observed; the remaining details are explicitly left to the application (Source: WICG Soft Navigations). In the final origin trial, replaceState was added as an additional trigger (Source: Chrome for Developers Blog) - relevant for every app that writes filter and sort state into the URL without creating a history entry.

User interaction

A trusted click or tap starts the process. Programmatic route changes without a user action - a redirect after load, for example - do not meet the condition.

Visible URL change

The address changes visibly for the user, via the History API or the Navigation API. Purely client-side filtering without a URL change does not count as a navigation.

Visible paint

The interaction leads to a contentful update that is actually painted. A pure state change without a visible result stays undetected.

Attribution across asynchronous boundaries is the technically demanding part. An interaction triggers a fetch whose response later modifies the DOM - the heuristic has to trace that change back to the original interaction. Chrome uses an internal task attribution for this, propagating the interaction context across fetch and setTimeout (Source: WICG Soft Navigations). A purely timer-based approach was rejected because users interact every few seconds, and timer cutoffs would miss precisely the slow outliers of four to ten seconds (Source: WICG Soft Navigations). For frontend optimization that is the decisive message: the chain of interaction, data fetch and rendering is treated as one unit.

The new entry types and the navigationId

Two new entry types are observable: soft-navigation is emitted after each detected soft navigation, and interaction-contentful-paint after interactions that cause a contentful paint (Source: Chrome for Developers). A soft-navigation entry carries the name of the new URL, a navigationId as a unique reference, the interactionId of the initiating interaction, startTime as the moment of the interaction, plus paintTime and presentationTime as the FCP of that navigation; the method getLargestInteractionContentfulPaint() returns the largest associated paint entry (Source: Chrome for Developers).

The real lever for route cutting, however, is the navigationId. It is attached to seven entry types: first-paint, first-contentful-paint, largest-contentful-paint, interaction-contentful-paint, first-input-delay, event and layout-shift (Source: Chrome for Developers). That makes it possible to assign every single layout shift and every interaction event to a specific route - exactly the mapping that was missing. One detail from the final origin trial matters here: interaction-contentful-paint was decoupled from soft navigations and is now emitted for all interactions, not just navigating ones; in addition, the largestInteractionContentfulPaint attribute was added to the SoftNavigationEntry (Source: Chrome for Developers Blog).

AspectHard navigationSoft navigation
Time origindocument load resets the reference pointstartTime of the entry must be subtracted yourself
TTFBreal value from navigation timingreported as 0 (Source: Chrome for Developers)
LCPlargest paint of the document loadvia getLargestInteractionContentfulPaint()
CLSstarts at 0reset to 0 at the route boundary
INPacross the entire lifecycleper route until the next navigation
CrUX reportingestablished, basis of the assessmentnot yet decided (Source: Chrome for Developers)

The time arithmetic is the most common trap. Timings are still returned relative to the original hard navigation - anyone needing values relative to the route has to subtract the soft navigation's startTime themselves (Source: Chrome for Developers). Forget that, and LCP values grow with session duration, making the third route look dramatically worse than the first without anything actually degrading. For teams already working on fast transitions, speculation rules for instant navigation are worth a look - that is about speeding up, this is about measuring.

Cutting LCP, INP and CLS per route

The goal is a dataset in which every route carries its own set of core metrics - as if it were a page of its own. For LCP, getLargestInteractionContentfulPaint() on the soft navigation entry provides the starting point; the associated interaction-contentful-paint entries are matched via the interactionId, explicitly not via the navigationId (Source: Chrome for Developers). The appropriate soft navigation start time is then subtracted (Source: Chrome for Developers).

For CLS and INP a simpler rule applies: both are initialised to 0 at each soft navigation and measured until the next navigation (Source: Chrome for Developers). In practice, the previous navigation's metrics are finalised as each new soft navigation occurs (Source: Chrome for Developers). That turns the session value into a sequence of route values - and a diffuse INP across twenty minutes becomes the insight that it is the cart transition of all things that stalls. How to actually fix INP once the culprit is found is covered in the article on INP and Interaction to Next Paint.

Entries at the route boundary

When entries span soft navigations, they may contain the previous or the next navigationId depending on when the entry was emitted (Source: Chrome for Developers). A layout shift occurring during the transition therefore does not necessarily land where you would expect. Anyone evaluating route values at sub-second resolution should know about this fuzziness and handle edge cases deliberately rather than dismiss them as measurement errors.

The CLS case deserves particular attention because it shows the distortion most clearly. Without route cutting, an SPA carries the worst shift burst of the entire session with it - regardless of which route produced it. With route cutting, it stays where it belongs. That fundamentally changes prioritisation, because it suddenly becomes visible which template causes the problem. The mechanics of the shifts themselves and their typical causes are covered in the article on avoiding Cumulative Layout Shift.

Setting up measurement with the PerformanceObserver

Getting started is unspectacular: a PerformanceObserver on the type soft-navigation with buffered: true also delivers entries that were already emitted (Source: Chrome for Developers). Feature detection matters so that older browsers pass through cleanly - check PerformanceObserver.supportedEntryTypes for soft-navigation or, alternatively, 'SoftNavigationEntry' in window (Source: Chrome for Developers).

soft-nav-rum.js
// Observe soft navigations and build per-route segments
let current = null;

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (current) finalise(current);          // close out previous route
    current = {
      id: entry.navigationId,
      url: entry.name,
      start: entry.startTime,                // new time origin
      lcp: 0, cls: 0, inp: 0
    };
    const icp = entry.getLargestInteractionContentfulPaint();
    if (icp) current.lcp = icp.startTime - entry.startTime;
  }
}).observe({ type: 'soft-navigation', buffered: true });

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!current || entry.navigationId !== current.id) continue;
    if (!entry.hadRecentInput) current.cls += entry.value;
  }
}).observe({ type: 'layout-shift', buffered: true });

One detail decides whether this works in long-lived apps: looking entries up via getEntriesByType is limited to the first 50 buffered entries - which is why an observer that receives entries as they arrive is the recommended method and is necessary for apps with more than 50 soft navigations (Source: Chrome for Developers). If you run an application where users pass through dozens of routes over hours, do not start with periodic polling in the first place. For SaaS applications with long sessions that is the normal case, not the exception.

  • Register an observer on soft-navigation instead of polling - because of the limit of 50 buffered entries (Source: Chrome for Developers).
  • Subtract the soft navigation's startTime from all timings, since values come back relative to the hard navigation (Source: Chrome for Developers).
  • Reset CLS and INP to 0 at every route boundary and finalise the preceding route (Source: Chrome for Developers).
  • Match LCP entries via the interactionId, not via the navigationId (Source: Chrome for Developers).
  • Treat TTFB per soft navigation as 0 and do not report it as an improvement (Source: Chrome for Developers).
  • Put feature detection first so browsers without support carry on unchanged (Source: Chrome for Developers).

The same discipline applies to sending the data as to any monitoring: sparingly, batched and without slowing the page down. Route values are only sent once the route is complete, and collected values are flushed when the page is left. That payloads can be compressed considerably is shown in the article on shared dictionaries and delta compression - relevant as soon as route telemetry reaches volume.

Common misreadings in practice

The most common disappointment: the heuristic does not fire where you expect it to. Purely client-side filtering without a URL change and without a visible paint is not detected as a navigation (Source: Chrome for Developers). A faceted search that only swaps the result list but leaves the address untouched therefore remains a silent process - functionally a page change for the user, technically not. Chrome says so openly: the definition can produce false positives that users would not really consider a navigation, as well as false negatives where the user very much assumes a page change but the criteria are not met (Source: Chrome for Developers).

The heuristic is an approximation, not a contract

Read the detection as an exact model of user perception and you will misinterpret the numbers. It is an approximation of a gut feeling - deliberately conservative, with known edges. Dealing with that is unspectacular: check which of your own route transitions meet the three conditions, and for the rest either set the URL properly or knowingly mark them as not captured in your reporting.

A second pitfall concerns element references. Wait too long to evaluate, for instance until page unload, and the nested LCP element reference may already have been garbage-collected, removed from the DOM or detached, returning null (Source: WICG Soft Navigations). If you want to log the triggering element, read it at the end of the route, not at the end of the session.

The third point is organisational. Route values are not comparable with previous session values - an SPA that suddenly reports clean per-route figures has not improved, it measures differently. If you make the switch, set a cut-off date, run both views in parallel for a while, and resist the temptation to sell the change as a success. What a lab tool can and cannot contribute is put in context by a Lighthouse audit: it measures the document load, not the route journey.

What the Chrome status leaves open on CrUX

Sobriety is called for here. The Chrome position is unambiguous: how exactly soft navigations will be reported in CrUX once the feature is launched is still to be determined (Source: Chrome for Developers). The final origin trial explicitly served to evaluate the API and not the question of how the data will be used in CrUX or tooling - that will be decided once the team is satisfied with the API and launches it outside the origin trial (Source: Chrome for Developers Blog). The trial ran from Chrome 147 to Chrome 149; from Chrome 151 the feature is available unflagged for all sites (Source: Chrome for Developers, Chrome for Developers Blog).

This implies an uncomfortable but important distinction: route measurement is possible in your own monitoring from Chrome 151, but no effect on the ranking-relevant assessment comes with it. Adopting the API brings insight into the real user journey - not automatically better field data in Search Console. That is not an argument against adoption but one for honest expectations.

Standardisation is also at an early stage. The work runs in the WICG as an incubation, Chromium has an experimental implementation, while no public signals exist yet for Gecko and WebKit (Source: WICG Soft Navigations). Measuring today means measuring in Chrome first - which matches the coverage of CrUX, but not the entire user base. The Web Almanac frames the direction: modern single-page websites often use JavaScript-based navigations, and support for soft navigations is expected to improve how Core Web Vitals are captured for these in-page transitions, providing a more accurate view (Source: HTTP Archive Web Almanac 2025).

How exactly soft navigations will be reported in CrUX, once the feature is launched, is also still to be determined.

Chrome for Developers, Measuring soft navigations

Context helps in judging the effort. In 2025, 48 percent of mobile websites and 56 percent of desktop websites achieved good Core Web Vitals (Source: HTTP Archive Web Almanac 2025). For LCP it was 62 percent on mobile and 74 percent on desktop, for CLS 81 percent on mobile and 72 percent on desktop (Source: HTTP Archive Web Almanac 2025). INP shows the highest figures at 77 percent on mobile and 97 percent on desktop (Source: HTTP Archive Web Almanac 2025) - of all metrics the one that runs across the whole session in SPAs and has so far lacked any route reference. How the thresholds and assessment rules have developed overall is summarised in the article on Core Web Vitals 2026. That AI crawlers additionally create measurable load and thereby shift the server side of the equation is covered in the article on load caused by AI crawlers.

From route measurement to optimization

Measuring is not an end in itself. The value of the Soft Navigations API only emerges when route values turn into decisions. The sequence is always the same: set up route measurement properly, collect the resulting LCP, INP and CLS values per route over several weeks, rank the routes by impact - traffic times distance from the threshold - and then work deliberately on the two or three templates that make the difference.

In practice the same patterns show up almost every time: a route transition waits on the data fetch instead of painting an intermediate view. A template loads its main image only after the framework code. A filter pushes the result list down because its height is not reserved. None of these problems is exotic - they were merely invisible as long as all routes collapsed into a single session value. Making them visible is precisely the purpose of a solid performance analysis.

We set up the route measurement, interpret the LCP, INP and CLS values that emerge per route, and derive the measures from them - from the loading behaviour of templates through interaction latency to the stability of the layout during route changes. That the results are then demonstrable rather than asserted is the actual point: only route cutting turns a diffuse session value into a statement you can work with. What this approach looks like in detail is shown by our web performance services.

Sources and studies

This article is based on data and findings from: Chrome for Developers - Measuring soft navigations (three-condition heuristic, entry types soft-navigation and interaction-contentful-paint, fields of the SoftNavigationEntry, navigationId on seven entry types, TTFB as 0, CLS and INP reset at the route boundary, timings relative to the hard navigation, limit of 50 buffered entries, feature detection, Chrome 151 unflagged, open CrUX reporting); Chrome for Developers Blog - Final Soft Navigations origin trial (origin trial from Chrome 147 to Chrome 149, decoupling of InteractionContentfulPaint, replaceState as a trigger, largestInteractionContentfulPaint attribute, purpose of the trial); web.dev (Google) - How SPA architectures affect Core Web Vitals (measurement relative to the top-level navigation, no reset on route changes, CLS behaviour compared to multi-page architectures, INP across the entire lifecycle, CrUX aggregation by origin or load URL); WICG Soft Navigations Specification (definition of the same-document navigation, criteria, task attribution across asynchronous boundaries, garbage collection of the LCP element reference, incubation status, signals from Gecko and WebKit); HTTP Archive Web Almanac 2025 (shares of good Core Web Vitals by device and metric, framing of JavaScript-based navigations). Information as of July 2026.