v139 · SPA Navigation Observer

SPA Navigation Observer

Navigate a simulated SPA and watch each client-side route change produce a SoftNavigationEntry with a unique navigationId — the key that ties LCP, INP, and CLS measurements to a specific route.

Origin Trial (Chrome 147–149). Enable chrome://flags/#soft-navigation-heuristics to see real SoftNavigationEntry objects. The demo detects support and falls back to a simulation when unavailable.
Detecting soft-navigation support…
https://shop.example/

Home

Hero image (LCP candidate)

Welcome to the simulated SPA. Click the nav items above to trigger soft navigations and observe the performance entries in the panel on the right.

Products

Product grid (LCP candidate)

Each route change fires a heuristic-detected soft navigation. The browser assigns a unique navigationId shared by all Web Vitals entries for this route.

Cart

Cart contents (LCP candidate)

The SoftNavigationEntry carries the new URL, interaction timestamp, and paint timing — everything you need to attribute vitals to the right route.

Account

Account summary (LCP candidate)

Notice how each entry gets a monotonically increasing interactionId. This links back to the pointer/keyboard event that triggered the navigation.

PerformanceObserver — 'soft-navigation'

Click a nav item in the SPA to trigger a soft navigation…

API code

// Observe soft-navigation entries
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.entryType);    // 'soft-navigation'
    console.log(entry.name);         // new URL, e.g. 'https://shop.example/products'
    console.log(entry.startTime);    // DOMHighResTimeStamp of the interaction
    console.log(entry.duration);     // time to "navigation complete" (LCP settled)
    console.log(entry.navigationId); // unique ID for this soft navigation
    console.log(entry.interactionId); // pointer/keyboard event ID that triggered it
  }
});

observer.observe({ type: 'soft-navigation', buffered: true });

// Correlate with other vitals entries using navigationId:
const lcpObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // entry.navigationId links this LCP to its SoftNavigationEntry
    console.log(`LCP ${entry.startTime}ms — nav ${entry.navigationId}`);
  }
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });

see also