DEV Community

Vishal Chauhan
Vishal Chauhan

Posted on

CrUX Now Counts SPA Soft Navigations. 5 Things I'd Check Before Search Console Switches

For years, the Chrome UX Report (CrUX) has had a blind spot. If your site is a single-page app, Chrome only recorded the first "hard" page load. Every route change after that was invisible, and every interaction and layout shift from the whole session got lumped onto the landing page.

That changed on October 13. Chrome started publishing experimental Core Web Vitals that include soft navigations in CrUX, following the launch of the Soft Navigations API in Chrome 151. Your stable scores haven't moved, and Search Console isn't using the new numbers yet. But this is the data Google says it will switch to, so now is a good time to look.

Here are the five things I'd check, with code you can run today.

First, what counts as a soft navigation

Chrome's definition has three conditions. All three have to happen:

  1. The navigation is started by a user action.
  2. The URL visibly changes.
  3. The interaction causes a visible paint.

When that happens, Chrome treats the new view as its own "page" for LCP, INP, and CLS. CrUX now has three new fields that blend those soft navigations with normal page loads:

  • experimental_largest_contentful_paint_with_softnavs
  • experimental_interaction_to_next_paint_with_softnavs
  • experimental_cumulative_layout_shift_with_softnavs

They're in the CrUX BigQuery dataset, the CrUX API, and the CrUX History API as of the October 2026 release.

1. Find out whether this even affects you

Most sites won't see a difference. According to the Chrome team, around 70% of origins in CrUX have fewer than 5% soft navigations, and only about 7–11% use soft navigations for more than half of their page views. If you serve a fresh HTML document on every click, your numbers should look the same.

The top of the web is a different story. Among the top 1,000 sites, 35% on Android and 42% on desktop rely on soft navigations for more than half of their page views.

The fastest way to check is CrUX Vis, which now has a toggle for the new metrics. If you want numbers you can script, call the CrUX API directly. This Node 18+ script compares the stable and experimental fields side by side for an origin or a single URL:

// crux-softnavs.mjs
// Usage: CRUX_API_KEY=... node crux-softnavs.mjs https://example.com PHONE
const key = process.env.CRUX_API_KEY;
const input = process.argv[2];
const formFactor = process.argv[3] ?? 'PHONE';

if (!key || !input) {
  console.error('Usage: CRUX_API_KEY=... node crux-softnavs.mjs <origin-or-url> [PHONE|DESKTOP|TABLET]');
  process.exit(1);
}

const pairs = [
  ['LCP', 'largest_contentful_paint', 'experimental_largest_contentful_paint_with_softnavs'],
  ['INP', 'interaction_to_next_paint', 'experimental_interaction_to_next_paint_with_softnavs'],
  ['CLS', 'cumulative_layout_shift', 'experimental_cumulative_layout_shift_with_softnavs'],
];

const parsed = new URL(input);
const isOrigin = parsed.pathname === '/' && !parsed.search;
const body = {
  ...(isOrigin ? { origin: parsed.origin } : { url: parsed.href }),
  formFactor,
  metrics: pairs.flatMap(([, stable, soft]) => [stable, soft]),
};

const res = await fetch(
  `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${key}`,
  { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
);

if (!res.ok) {
  // A 404 usually means CrUX doesn't have enough data for this origin/URL.
  console.error(res.status, await res.text());
  process.exit(1);
}

const { record } = await res.json();

function summary(metric) {
  const m = record.metrics[metric];
  if (!m) return 'no data';
  const p75 = Number(m.percentiles.p75); // CLS p75 comes back as a string
  const good = (m.histogram[0].density * 100).toFixed(1);
  return `p75 ${p75}, ${good}% good`;
}

for (const [label, stable, soft] of pairs) {
  console.log(`${label.padEnd(4)} stable: ${summary(stable).padEnd(24)} with soft navs: ${summary(soft)}`);
}
Enter fullscreen mode Exit fullscreen mode

You'll need a CrUX API key from Google Cloud. Run it for PHONE and DESKTOP separately, since your mobile and desktop users can browse very differently.

If the two columns are nearly identical, you can mostly stop here. If they're far apart, keep going.

2. Make sure your router actually triggers detection

The browser decides what counts, not your framework. That's the point, because it works for existing SPAs without anyone calling a special API. It also means some route changes you think of as navigations won't count, and some things you don't think of as navigations will.

Paste this into the DevTools console on a supported Chrome (151+) and click around your app:

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('soft nav', entry.name, 'id', entry.navigationId, 'at', Math.round(entry.startTime), 'ms');
  }
}).observe({ type: 'soft-navigation', buffered: true });
Enter fullscreen mode Exit fullscreen mode

Things I'd watch for:

  • Route changes with no user action. A redirect after a data fetch or a timed carousel that rewrites the URL fails the "user action" condition, so it won't show up.
  • Clicks that change the URL but don't paint. If your app updates the URL and then waits on data before rendering anything visible, check whether a soft navigation is reported at all.
  • Things that probably shouldn't count but might. A filter panel that rewrites the query string on every click and repaints the list can meet all three conditions. Chrome openly says the definition can produce false positives and false negatives, and it wants feedback on the soft navigations spec repo.

You can also see this without code. Chrome 152 added soft navigation support to Live Metrics in the DevTools Performance panel, and Chrome 153–154 brought it to Insights as well.

3. Start measuring both ways in your own RUM

CrUX gives you 28-day aggregates. To debug, you need your own real-user data. The web-vitals library supports soft navigations from v6.0.0 (6.2.3 is the current release on npm), behind a reportSoftNavs: true option.

There's a catch the README calls out. Turning on reportSoftNavs changes how the first page load is measured too: once the first soft navigation happens, the initial URL's CLS and INP get finalized early. If you just flip the flag, your historical trend lines will shift for reasons that have nothing to do with performance.

Google's own suggestion is to measure both ways and store both. Here's a minimal version:

import { onCLS, onINP, onLCP } from 'web-vitals';

const softNavSupported =
  PerformanceObserver.supportedEntryTypes?.includes('soft-navigation') ?? false;

function report(method) {
  return (metric) => {
    const body = JSON.stringify({
      method,                              // 'hard' or 'soft'
      softNavSupported,
      name: metric.name,
      value: metric.value,
      rating: metric.rating,
      id: metric.id,
      navigationType: metric.navigationType, // 'soft-navigation' for soft navs
      navigationId: metric.navigationId,
      url: metric.navigationURL ?? location.href,
    });
    if (!navigator.sendBeacon?.('/rum', body)) {
      fetch('/rum', { method: 'POST', body, keepalive: true });
    }
  };
}

// The way you've always measured
onLCP(report('hard'));
onINP(report('hard'));
onCLS(report('hard'));

// Per soft navigation
onLCP(report('soft'), { reportSoftNavs: true });
onINP(report('soft'), { reportSoftNavs: true });
onCLS(report('soft'), { reportSoftNavs: true });
Enter fullscreen mode Exit fullscreen mode

Two details matter here:

  • Use navigationURL, not location.href. Metrics for a soft navigation can be reported after the user has already moved to the next route. The library tells you which URL the metric belongs to. The fallback above is only for older browsers.
  • Segment by navigationType. Soft navigations reuse code and assets that are already loaded, so they behave differently from cold loads of the same URL. Mixing them in one chart hides problems in both.

Only Chromium 151+ supports the API. Other browsers will keep reporting the old way even with the flag on, which is why I tag softNavSupported on every beacon.

4. Expect better LCP, but don't relax about INP

For the sites that are mostly soft navigations, the shift can be big. In Chrome's analysis, Android origins with 75–100% soft navigations went from 26% "good" LCP to 87% once soft navigations were included. Across all origins in the October release, the share passing all three Core Web Vitals went from 55.9% to 59.5%.

Chrome is careful to explain why. SPAs aren't faster than multi-page sites overall. Follow-up clicks are faster on any site, and until now SPAs only got credit for their slowest load.

INP is the one I'd watch. It improves because responsiveness is now measured per view instead of taking the worst interaction across a whole session, which roughly doubles the Android pass rate for mostly-soft sites. Even so, Chrome says SPA-heavy sites still trail multi-page sites on "good" INP, and heavy main-thread JavaScript during route transitions can still cause problems.

One quirk to know when you debug: the click that triggers a soft navigation is usually counted toward the page you're leaving, not the one you're going to. A slow route transition will show up as bad INP on the previous URL.

LCP has a quirk too. For a soft navigation, only new paints from the triggering interaction count. If a big banner stays on screen between routes, it won't be the LCP element for the soft navigation. Deep-link to that same URL, and the banner is a fresh paint and can become the LCP element. Same URL, different LCP element, depending on how the user got there.

5. Put the dates on your calendar

Here's the timeline Google has published so far:

  • October 13, 2026: experimental LCP, INP, and CLS with soft navigations land in CrUX.
  • November 2026 CrUX release: Chrome expects to add soft-navigation versions of FCP and navigation types.
  • May 2027 (target): the metrics graduate from experimental, subject to feedback. After that, Search Console switches to the new data.

Chrome also says to expect month-to-month shifts while the measurement is still experimental, so don't build alerts on a single month's change.

The short version

If you run a multi-page site, this probably doesn't change much for you. If you run an SPA, run the CrUX comparison, check that your router actually triggers soft navigations, and start collecting both kinds of measurements in your own monitoring now. That gives you about six months of your own data before Search Console makes the switch, and you'll know which routes are slow instead of guessing.

Top comments (0)