Santaji GadeCore Web Vitals1 week ago10 Views

A green Lighthouse score on the homepage does not mean real users have a fast experience once they start clicking through routes. This guide explains why Core Web Vitals for single page applications behaves differently, what soft navigations change, and which fixes actually move the numbers.
Table of Contents
ToggleHey! If your React or Vue app looks fast in a lab test but real visitors keep complaining that tapping around feels sluggish, you are not imagining things, and this one is for you.
Picture this: you ship a shiny new app, Lighthouse hands you a green score on the homepage, and then real users start clicking through routes and everything suddenly feels heavy. That gap between a lab score and real user frustration is exactly where Core Web Vitals for single page applications gets complicated, and it is worth understanding before your team spends another sprint chasing the wrong number.
Most Core Web Vitals advice assumes a traditional page load: the browser requests a document, the server streams HTML, and the page settles. A single page app built with React, Vue, or Angular works differently. One bundle boots up once, then quietly rewrites the DOM every time someone clicks a link, and the browser never fires another full navigation.
That architecture is useful for app like interfaces, but it breaks an assumption baked into the original metrics: that a page load and a page view are the same event. Google's own engineering team has said plainly that single page apps have "never been fully supported" by these metrics, since the browser has no clean signal for where one screen ends and the next begins.
In practice this shows up as two pain points: Interaction to Next Paint getting worse the deeper someone navigates, and Largest Contentful Paint looking fine on the first load but meaningless on every route after that. This guide on Core Web Vitals for single page applications walks through why both happen and which fixes actually move the needle.
Interaction to Next Paint measures how long the browser stays frozen after someone taps, clicks, or types, until the next frame paints. On a server rendered site that mostly means keeping handlers light. In a single page app it is bigger, since one click on a nav link can trigger route matching, data fetching, component teardown, a fresh render pass, and rehydration, all on the same main thread.
Vercel's own engineering team documented this clearly while working on nextjs.org itself: before they adopted React 18's selective hydration with Suspense, loading a full page of JavaScript before any component could respond to input was pushing Total Blocking Time to 430 milliseconds. After moving to Suspense boundaries that let the browser do other work while hydration finished in the background, that figure dropped to 80 milliseconds, and the resulting INP came in at a comfortably good 48 milliseconds.
That is the lesson for anyone chasing a better INP score inside a framework heavy app: the problem is rarely one slow function, it is the sheer amount of synchronous work a route change asks of the main thread in one burst.
Tip
Record a DevTools trace while clicking through your busiest in app routes, and look for long tasks over 50 milliseconds right after the click. That trace tells you more about real INP than any lab score.
Largest Contentful Paint measures how soon the biggest visible element shows up on screen, and a passing score needs under 2.5 seconds. Client rendered apps tend to fail this quietly: the server sends back a near empty HTML shell, the browser has to download, parse, and execute the JavaScript bundle before a single word of real content exists, and only then does the framework build the DOM and paint something meaningful.
Google's own documentation on client rendering spells out why this hurts: unlike HTML streamed from a server, which the browser's preload scanner can act on immediately, resources referenced inside JavaScript generated markup stay invisible to that scanner until the script runs. Images, fonts, and hero content effectively queue up behind the entire bundle instead of loading in parallel with it.
This is why LCP guidance written for ordinary pages can feel incomplete for a single page app. A static site worries about image compression and font loading; a client rendered app solves those problems while also racing a JavaScript bundle just to get content on the canvas.
Astro's 2023 Web Framework Performance Report, built on real Chrome User Experience Report data, found that only roughly one in four Next.js sites and one in five Nuxt sites passed the overall Core Web Vitals assessment, while the industry wide INP pass rate sat at 60.9 percent. Astro credited the gap to how much client side JavaScript these architectures ship by default.
Here is the part most teams never realize: the Chrome User Experience Report, the real world dataset behind the ranking signal, historically only measured the first hard page load of a session. Every route change afterward, no matter how slow, was invisible to that field data, because the browser never recorded a traditional navigation event for it.
Chrome engineers call these in app transitions soft navigations, and they built a dedicated detection system to finally close the gap. A soft navigation now has to meet three conditions: a real user interaction kicked it off, the visible URL changed, and the browser actually painted something new as a result. Chrome 151 shipped standardized APIs for this, including PerformanceSoftNavigation and a new InteractionContentfulPaint entry that lets LCP be measured per route instead of only once per session.
For anyone building a RUM dashboard, this matters enormously. As DebugBear's breakdown of the feature puts it, every metric effectively resets at each soft navigation: LCP and CLS start fresh, INP measures delay from that point forward, and Time to First Byte reports as zero. Tools opt into this with reportSoftNavs: true inside Google's web-vitals library, version six and above, rather than aggregating an entire session into one misleading number.
Google's performance team is careful to say neither architecture is inherently superior. Web.dev's own FAQ on SPA architecture states that single page apps and traditional multi page sites "are both capable of delivering high quality experiences to users," and that switching frameworks purely to chase a score is the wrong motivation.
The single highest leverage fix for both INP and LCP is also the simplest: stop shipping the entire app in one JavaScript file. Web.dev's guidance on reducing JavaScript payloads recommends splitting a bundle along route boundaries, so a visitor landing on the homepage only downloads code the homepage actually needs, not the checkout flow or every modal in the product.
In React this usually means pairing React.lazy with Suspense around each route component. Angular ships route level code splitting as a framework default, and Vue Router supports the same pattern with dynamic imports. A smaller initial bundle means a faster first LCP, and smaller per route chunks mean less code competing for the main thread when INP is measured.
Tip
Run your production bundle through your framework's bundle analyzer first. Teams are often surprised to find one unused charting library accounting for a third of the JavaScript shipped on every route.
The second major fix tackles LCP directly by removing the wait for JavaScript on first load. Next.js documentation on rendering strategies lays out the tradeoffs: static generation builds HTML at build time, server rendering builds it fresh per request, and both ship real content in the first response instead of an empty shell. A hybrid setup, where marketing pages use static rendering while a logged in dashboard stays client rendered, gets speed benefits where they matter most.
Google's documentation on JavaScript heavy sites backs this up from the crawling side too: Googlebot queues a page, runs it through a headless browser, and only then discovers links added by client rendered content, a two wave indexing cycle that can lag the initial crawl noticeably. Server or static rendering sidesteps that delay.
The fourth fix lives inside the route transition itself: reduce how much synchronous work a single click triggers. Defer analytics and non critical widgets until after the next paint, break large render functions into smaller chunks the browser can interrupt, and use CSS transitions instead of JavaScript animation. Every millisecond trimmed there is a millisecond INP improves.
None of the previous four fixes mean much if the measurement itself is broken, which brings us to the fifth and final fix: instrument your app with a library that actually understands soft navigations. Using a current release of Google's web-vitals library with soft navigation reporting on, pointed at a proper measurement setup, is the only reliable way to see how individual routes perform rather than one blended session average.
Smashing Magazine's field guide to monitoring Core Web Vitals is a useful reminder of the thresholds above: LCP under 2.5 seconds, INP under 200 milliseconds, and CLS under 0.1. Chasing Core Web Vitals for single page applications without those numbers in front of you is just guessing.
It refers to the specific challenges of measuring and improving LCP, INP, and CLS inside apps built with React, Vue, or Angular, where one JavaScript bundle handles most page changes instead of the browser loading a fresh document each time.
Each route change can trigger data fetching, component teardown, and a fresh render pass on the main thread in response to a single click, so the browser stays busy longer before it can paint the next frame, which is exactly what INP measures.
Not always, but it usually adds a delay, since the preload scanner cannot discover images or text buried inside JavaScript generated markup the way it can with HTML streamed from a server.
A soft navigation is an in app route change, triggered by a user interaction, that updates the URL and visible content without a full page reload. Field data historically ignored these, so Chrome built dedicated APIs so tools can measure each one.
It solves most of the first load LCP problem by shipping real content immediately, but it will not fix a heavy route transition by itself, which is why pairing it with code splitting matters for the full picture.
Interaction to Next Paint, the Core Web Vitals metric that measures how long a page stays unresponsive after a user interaction.
Largest Contentful Paint, the time it takes for the biggest visible element on a screen to actually render.
A route change inside a single page app that updates the URL and content without a full browser page reload.
Breaking one large JavaScript bundle into smaller pieces that load only when a specific route or feature is needed.
The process where a framework attaches event handlers and interactivity to HTML that already exists on the page.
Using server rendering or static generation for some pages and client rendering for others within the same app.
Explore more Brandella Journal guides on SEO and digital marketing.








