← Chrome 151 reference

v151 · web api · shipped

PerformanceSoftNavigation and InteractionContentfulPaint performance entry

New PerformanceEntry types that let you measure Core Web Vitals — LCP, CLS, INP — for single-page application navigations that happen without a full page reload.

at a glance

Shipped inChrome 151 (desktop + Android)
StatusEnabled by default
FlagNone (was chrome://flags/#soft-navigation-heuristics before Chrome 150)
Origin trialChrome 147–149 (final OT); shipped in 150
SpecSoft Navigations — WICG
ExplainerWICG / soft-navigations
ChromeStatus5144837209194496 — PerformanceSoftNavigation and InteractionContentfulPaint performance entry

why it exists

Single-page applications (SPAs) have always been invisible to the Core Web Vitals (CWV) framework. CWV metrics like Largest Contentful Paint, Cumulative Layout Shift, and Interaction to Next Paint are defined relative to a navigation, and traditional navigation timing only fires on full page loads. When a React Router, Next.js, or SvelteKit app pushes a new URL and re-renders in-place, the browser saw no navigation — so no fresh CWV measurement was started. This meant a whole class of web experiences had no performance signal in Search Console, PageSpeed Insights, or field data.

Soft navigations defines a heuristic: if the user interacts (click, keyboard, etc.), the URL changes via History API or Navigation API, and significant DOM updates follow, the browser records a soft-navigation entry and resets the CWV baseline for that navigation scope.

Source: WICG soft-navigations explainer and Chrome developer docs.

shape of the API

Two new entry types are added to the Performance Timeline. Both are observed via the existing PerformanceObserver interface.

soft-navigation entry

Reported once per detected soft navigation. Extends PerformanceEntry.

entryType"soft-navigation"
nameThe URL of the destination (after the pushState / Navigation API)
startTimeTime of the user interaction that triggered the navigation
durationTime from interaction to the browser considering the navigation complete
navigationIdA unique string that groups all performance entries belonging to this soft navigation. Also added to all subsequent PerformanceEntry types (LCP, layout-shift, event-timing, etc.) within the navigation scope.

interaction-contentful-paint entry

Analogous to largest-contentful-paint but scoped to a soft navigation. Extends PerformanceEntry.

entryType"interaction-contentful-paint"
renderTimeWhen the largest new element painted after the interaction
sizeSize in pixels of the candidate element, same algorithm as LCP
elementReference to the painted DOM element
navigationIdLinks to the triggering soft-navigation entry

navigationId on existing entries

All PerformanceEntry types gain a navigationId property. During the initial hard navigation its value is the same as performance.now() epoch identifier. After each soft navigation it changes to the new soft navigation's ID, so you can slice CLS, INP, and event-timing by navigation.

Source: WICG Soft Navigations spec.

examples

observe soft navigations

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log("Soft navigation to:", entry.name);
    console.log("  Started:", entry.startTime);
    console.log("  Navigation ID:", entry.navigationId);
  }
}).observe({ type: "soft-navigation", buffered: true });

measure per-navigation LCP

const lcpByNav = new Map();

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Attribute to the current navigation scope.
    lcpByNav.set(entry.navigationId, entry.renderTime || entry.loadTime);
  }
}).observe({ type: "largest-contentful-paint", buffered: true });

// interaction-contentful-paint gives ICP for soft navigations:
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    lcpByNav.set(entry.navigationId, entry.renderTime);
    console.log("ICP after soft nav:", entry.navigationId, entry.renderTime);
  }
}).observe({ type: "interaction-contentful-paint", buffered: true });

slice CLS per navigation

const clsByNav = new Map();

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.hadRecentInput) continue;
    const id = entry.navigationId;  // new in Chrome 151
    const prev = clsByNav.get(id) ?? 0;
    clsByNav.set(id, prev + entry.value);
  }
}).observe({ type: "layout-shift", buffered: true });

reading buffered entries

You can also query buffered entries directly without an observer:

// All soft navigations so far:
const softNavs = performance.getEntriesByType("soft-navigation");

// All ICP entries:
const icpEntries = performance.getEntriesByType("interaction-contentful-paint");

browser support

Chrome / Edge150 (enabled by default); flag available from 147
FirefoxNo signal
SafariNo signal
Source: chromestatus.com browser views, May 2026.

see also