Interaction Contentful Paint: Finally Measuring What Happens Inside a SPA
Chrome 151 (July 2026) shipped two performance entries — interaction-contentful-paint and soft-navigation — that close the SPA blind spot. Here's how we use them at Mintec to attribute INP and LCP to specific routes.
Interaction Contentful Paint: Finally Measuring What Happens Inside a SPA
Chrome 151 (stable since July 28, 2026) shipped two performance entries — interaction-contentful-paint and soft-navigation — that close the historical blind spot of single-page applications: until now it was impossible to measure Core Web Vitals per route inside a SPA. With these APIs you can attribute interaction latency to a concrete contentful paint, even when that content arrives through an async fetch.
This isn't a cosmetic DevTools improvement. It's the first time the browser understands "navigation" inside a document that never reloaded. And it changes how we diagnose — and argue about — SPA performance.
The blind spot that existed for a decade
Core Web Vitals were designed around the idea of a "page": the user requests a URL, the browser paints it, and the measurement starts and ends there. SPAs broke that model over a decade ago: the user clicks, JavaScript swaps the content, the URL changes via history.pushState, and no reload ever restarts the clock. As Chrome's own engineers document, the single-page application architecture was never fully supported by the Core Web Vitals metrics.
In practice, that meant two problems for anyone running a dashboard, a multi-step checkout, or a client app:
- Page-level INP mixed every route together. An INP of 340ms in the red didn't tell you whether the culprit was login, catalog, or cart. Every audit started with guesswork.
- LCP of a "page" meant LCP of the first load. The LCP of
/products/123when the user opened it with an internal click was never measured. Navigation performance = invisible.
With Long Animation Frames we could find guilty scripts inside a single interaction, but we still couldn't answer the granularity question: which route is slow?
What Chrome 151 ships exactly
Chrome 151 adds two entry types to the performance timeline:
interaction-contentful-paint(ICP): emitted when a user interaction produces new contentful paint inside the DOM regions that interaction modified. The project was originally called "Interaction to Contentful Paint", and it's deliberately different from INP: it doesn't measure when the browser paints something — it measures when it paints new content in the right place. It works even if the content arrives through an async fetch that resolves 400ms after the interaction.soft-navigation: emitted when a soft navigation is detected — a user-initiated state change that updates the URL and history without reloading the document. Each entry carries anavigationId, the new URL, and theinteractionIdof the initiating interaction, and establishes a new time origin so subsequent performance entries (first-paint, first-contentful-paint, largest-contentful-paint, event, layout-shift) are attributed to that navigation.
On top of that, the soft-navigation entry exposes getLargestInteractionContentfulPaint(), which retrieves the largest ICP for that navigation — the functional equivalent of LCP for a SPA route. The feature ships unflagged for all sites starting in version 151, following an origin trial that wrapped up in Chrome 147.
INP vs ICP vs soft-navigation vs LoAF: when to use each
The most common mistake is treating ICP as "the new INP". It isn't: they're complementary tools with different granularities.
| Tool | What it measures | Granularity | Support | When to use it |
|---|---|---|---|---|
| INP | Interaction-to-next-paint time (one number per page) | Full page | Chrome, Safari, Firefox (CWV) | Global monitoring, business thresholds |
| interaction-contentful-paint | Contentful paints in the interacted region | Individual interaction | Chrome 151+ | Route and component attribution in SPAs |
| soft-navigation | Same-document URL/state changes initiated by the user | Route | Chrome 151+ | Per-route performance baseline, internal-navigation LCP |
| LoAF | Animation frames >50ms and the scripts causing them | Frame/task | Chrome/Chromium | Root cause inside a slow interaction |
Our workflow is now: INP to know something is wrong, soft-navigation to know which route, ICP to know which paint, LoAF to know which script. Four zoom levels that didn't exist together before.
How to instrument it in production
You don't need a library. A PerformanceObserver in your client bundle (or an inline script if you want to capture from the very first moment):
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === "soft-navigation") {
console.log("Soft navigation:", entry.name, entry.navigationId);
// Route-level LCP equivalent
entry.getLargestInteractionContentfulPaint?.().then((icp) => {
console.log("Route painted at:", icp?.startTime, "ms");
});
}
if (entry.entryType === "interaction-contentful-paint") {
console.log("ICP:", entry.startTime, "interaction:", entry.interactionId);
}
}
});
observer.observe({ type: ["soft-navigation", "interaction-contentful-paint"], buffered: true });
On a client's Next.js admin panel, this instrumentation gave us in one week what a month of manual profiling hadn't: the orders route had a median ICP of 780ms because the table's fetch was waiting on a charting chunk that view never needed. The page-level INP masked it because every other route was fast. With the per-route data, we deferred the chunk and ICP dropped to 210ms without touching anything else.
The framework we use (three steps)
- Instrument and store per route. Observe both entries, persist
navigationId+ URL + ICPstartTimeto your RUM (or a dev log). One week of data is enough for a baseline. - Rank routes by median ICP. Not by aggregate INP. Routes with the worst ICP are where users perceive "the app doesn't respond", even when aggregate INP looks acceptable. That gap is exactly what these metrics reveal.
- Fix per route, not per global component. Each slow route gets its own chunks, fetches, and priorities. Verify with the same observer that the route's ICP dropped below the 75th percentile of your baseline (we use 300ms as an initial target, aligned with INP's "good" criterion).
What this changes for the SPA vs MPA decision (our take)
This doesn't revive the "SPA vs MPA" debate — it clarifies it. For years, part of the case against SPAs was "you can't measure them." That argument died in July 2026. Now the question is business-driven: if your SPA has routes with bad ICP, you have the tool to find and fix them. And if you're deciding architecture, this is one more reason to prefer hybrid renders like Astro with islands: less soft-navigation surface to measure, because most of the site behaves like an MPA.
The important caveat: this is Chrome/Chromium-only for now. Safari and Firefox don't support it, so don't use it as your only RUM source of truth — use it as a deep diagnostic layer on top of the field INP we already measure across browsers. The strategy we recommend in our INP guide doesn't change: thresholds and the LoAF/scheduler.yield() debugging flow remain the foundation. What changes is that you can now answer "which route?" with data, not gut feel.
The conclusion is simple: the browser finally knows what happens inside a SPA — and we no longer have an excuse not to know it either.
Frequently Asked Questions
What is InteractionContentfulPaint (ICP)?
It's a performance entry that Chrome 151 shipped in July 2026. It reports new contentful paints that occur inside the DOM regions modified by a user interaction, so you can attribute visual response to specific interactions — even when the content arrives through an async fetch.
What's the difference between INP and ICP?
INP measures the total time from interaction to next paint and produces one number per page. ICP reports specific contentful paints inside the interacted region and can be associated with soft navigations, giving route-level granularity that page-level INP can't offer in single-page applications.
Which browsers support soft-navigation and interaction-contentful-paint?
Only Chrome and Chromium for now, starting with version 151 (July 28, 2026). Safari and Firefox don't support them yet, so use them as a deep diagnostic layer in Chrome and keep field INP/LCP as the cross-browser source of truth.
What is a soft navigation?
A user-initiated state change of the document that updates the URL and pushes history entries without reloading the page — exactly what a SPA does on every route change. Chrome defines its boundaries with precise criteria (user interaction, URL change, history update) so Core Web Vitals can be measured for that kind of navigation.



