← All posts
Deep dive

Your SPA's vitals were invisible. Soft navigations change that.

An SPA can load its shell quickly while later route changes feel slow. Initial-load LCP does not describe every transition, while INP and CLS can span interactions across the page lifecycle. Measure the journeys people actually use.

What actually changed

Chrome's soft-navigation guidance, updated September 2026, describes APIs enabled by default from Chrome 151. They identify qualifying user-initiated URL changes with visible paint and support per-navigation performance measurement. Browser and RUM-provider support still vary.

That does not mean CrUX already reports every SPA transition. The same guidance says the form of CrUX soft-navigation reporting remains to be determined. Keep local or RUM measurements distinct from Search Console field reports.

The published Core Web Vitals targets are 2.5 seconds for LCP, 200 milliseconds for INP, and 0.1 for CLS. Compare your own measurements before and after a change; the original secondary-source claim of a universal 18–25% methodology regression is not a basis for your performance budget.

The consequence: you own the transition now

For a server-rendered site, "make the page fast" is a load-time problem: TTFB, critical CSS, image weight. For an SPA, the load is the easy part. The hard part is what happens on the tenth click: the route handler that fetches, the component tree that re-renders, the virtualized list that rebuilds, the main-thread work that blocks the tap the user just made. That's where INP lives, so measure those interactions explicitly.

So the work isn't cosmetic. You can't fix a transition you can't see, and you can't defend a number you've never measured. Three things have to be true, and none of them is a dashboard you install and forget.

1. Trend the real-user vitals over time

Field vitals are a lagging signal: CrUX reports a rolling window, so a regression you ship today surfaces in the data weeks later, aggregated as metric percentiles for URL/origin groups. That's fine for confirming a trend and useless for catching the deploy that caused it. The field number stays Google's; what you can own is the moment of change. Scheduled runs drive the same route transitions in real browsers on every deploy, with explicit performance budgets where appropriate; a slower transition need not fail a functional assertion, or shows its stall plainly in the trace, the night it ships rather than weeks later in a rolling average. And server-side production telemetry, trending CPU, memory, sessions, and errors over time, catches the backend shift that so often rides along with a front-end regression, in a chart you own on your schedule instead of Google's.

2. Test the transitions themselves

A vitals chart tells you a transition got slower. It doesn't tell you the transition still works. Those are different failures and you need both covered, because a soft navigation that renders the wrong destination state is a correctness bug the field metric will happily score as fast.

This is where SPA testing quietly separates the honest tests from the flaky ones. An SPA transition need not load a new document. Playwright’s waitForNavigation is deprecated and inherently racy; History API URL changes can count as navigation and return null, but registering the wait after clicking can miss them. Prefer URL and destination-state assertions:

// Fragile: a wait registered after the click can miss the transition
await page.getByRole('link', { name: 'Invoices' }).click();
await page.waitForNavigation(); // deprecated/racy; can miss a History API change

// Honest: wait for the destination state to actually render
await page.getByRole('link', { name: 'Invoices' }).click();
await expect(page).toHaveURL(/\/invoices/);
await expect(page.getByRole('heading', { name: 'Invoices' })).toBeVisible();

That auto-retrying expect is the whole trick. It re-samples the DOM until the destination is really there or it times out, which is exactly the discipline soft navigations demand, since the render lands whenever the fetch and re-render happen to finish rather than at a fixed moment. Lean on web-first assertions and a transition test waits for the truth instead of guessing a duration; reach for a waitForTimeout and you've written a test that flakes the day the route fetch runs 200ms slower. Those SPA-specific patterns for client routing, hydration, and virtualized lists cover the route transitions worth measuring, so the tests you write to keep transitions correct and the metrics you watch to keep them fast finally point at the same surface.

3. Load-test the browser-experienced journey

Browser measurements exercise client work that protocol traffic cannot directly observe. Server load can slow API responses and journey completion, but users’ browsers do not share one main thread just because they use the same server. Browser-VU step timings are not field INP; instrument interaction performance explicitly and compare with real-user data.

The through-line

The 2026 changes didn't invent SPA slowness. They add a local measurement mechanism; do not claim Search Console already scores every transition. The fix here is unglamorous: the same testing loop you already run, aimed one layer deeper. Trend the real vitals so a regression surfaces as a step in a chart. Test each transition with assertions that wait for the destination state, so "it navigated" actually means the right screen rendered. Load-test the journey in a real browser, alongside explicit interaction measurements and field data. Get those three pointed at the same route change, and when the metric goes green it's the tenth click that earned it, not the first paint.

Try it against your own app

TestVibe generates real Playwright tests from plain-language descriptions of the transitions you care about, verifies them against a live run before you rely on them, and pairs that with server-side production telemetry and browser-VU load tests so the same route change is covered for correctness, watched in production, and pressure-tested under concurrency. Get early access, or read how testing React SPAs handles SPA interaction and navigation failure modes.

Coming soon

Turn a described flow into a reviewable test.

Describe a flow in plain English. Generate a Playwright test, run it on your schedule, and review proposed fixes when your UI changes. Join the waitlist for early access.

  • Plain English in, real Playwright out
  • Runs in your CI, on your schedule
  • No credit card, unsubscribe anytime