Published: February 1, 2023, Last updated: July 22, 2025
Since its launch, the Core Web Vitals initiative has sought to measure the actual user experience of a website, rather than technical details behind how a website is created or loaded. The three Core Web Vitals metrics were created as user-centric metrics—an evolution over existing technical metrics such asDOMContentLoaded
or load
that measured timings that were often unrelated to how users perceived the performance of the page. Because of this, the technology used to build the site shouldn't impact the scoring providing the site performs well.
The reality is always a little trickier than the ideal, and the popular Single Page Application architecture has never been fully supported by the Core Web Vitals metrics. Rather than loading distinct, individual web pages as the user navigates about the site, these web applications use so-called "soft navigations", where the page content is instead changed by JavaScript. In these applications, the illusion of a conventional web page architecture is maintained by altering the URL and pushing previous URLs in the browser's history to allow the back and forward buttons to work as the user would expect.
Many JavaScript frameworks use this model, but each in a different way. Since this is outside of what the browser traditionally understands as a "page", measuring this has always been difficult: where is the line to be drawn between an interaction on the current page, versus considering this as a new page?
The Chrome team has been been considering this challenge for some time now, and is looking to standardize a definition of what is a "soft-navigation", and how the Core Web Vitals can be measured for this—in a similar way that websites implemented in the conventional multi-page architecture (MPA) are measured.
We've been working hard on improving the API since the last origin trial and are now asking for developers to try the latest iteration and provide feedback on the approach before this launches.
What is a soft navigation?
We have come up with the following definition of a soft navigation:
- The navigation is initiated by a user action.
- The navigation results in a visible URL change to the user, and a history change.
- The navigation results in a DOM change.
For some sites, these heuristics may lead to false positives (that users wouldn't really consider a "navigation" to have happened) or false negatives (where the user does consider a "navigation" to have happened despite not meeting these criteria). We welcome feedback at the soft navigation specification repository on the heuristics.
How does Chrome implement soft navigations?
Once the soft navigation heuristics are enabled (more on this in the next section), Chrome will change the way it reports some performance metrics:
- A
soft-navigation
PerformanceTiming
event will be emitted after each soft navigation is detected. - A new
interaction-contentful-paint
will be emitted after interactions that cause a meaningful paint. This can be used to measure Largest Contentful Paint (LCP) for soft navigations when such a paint spans a soft navigation. Note the original implementation of this reset thelargest-contentful-paint
metric allowing it to be re-emitted but we have settled on this alternative approach for simplicity but also greater future scope. - A
navigationId
attribute will be added to each of performance timings (first-paint
,first-contentful-paint
,largest-contentful-paint
,interaction-contentful-paint
,first-input-delay
,event
, andlayout-shift
) corresponding to the navigation entry the event was related to, allowing Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS) and Interaction to Next Paint (INP) to be calculated and attributed to the correct URL.
These changes will allow the Core Web Vitals—and some of the associated diagnostic metrics—to be measured per page navigation, though there are some nuances that need to be considered.
What are the implications of enabling soft navigations in Chrome?
The following are some of the changes that sites owners need to consider after enabling this feature:
- CLS and INP metrics may be sliced by soft navigation URL, rather than be measured over the duration of the whole page lifecycle.
- The
largest-contentul-paint
entry is already finalized on an interaction, so is only used to measure for the initial "hard" navigation LCP so no additional logic is needed to change how that is measured. - The new
interaction-contentful-paint
metric will be emitted from interactions, which may be used to measure LCP for soft navigations. - To attribute soft navigations to the correct URL, the new
navigationID
attribute on your performance entries may need to be considered in your application code using these entries. - Not all users will support this soft navigation API, particularly for older Chrome versions, and for those using other browsers. Be aware that some users may not report soft-navigation metrics, even if they report Core Web Vitals metrics.
- As an experimental new feature that is not enabled by default, sites should test this feature for unintended side-effects.
Check with your RUM provider if they support measuring Core Web Vitals by soft navigation. Many are planning on testing this new standard, and will take the previous considerations into account. In the meantime, some providers also allow limited measurements of performance metrics based on their own heuristics.
For more information on how to measure the metrics for soft navigations, see the Measuring Core Web Vitals per soft navigation section.
How do I enable soft navigations in Chrome?
Soft navigations are not enabled by default in Chrome, but are available for experimentation by explicitly enabling this feature.
For developers, this can be enabled by turning on the Experimental Web Platform features flag at chrome://flags/#soft-navigation-heuristics
or by using the --enable-features=SoftNavigationHeuristics:mode/advanced_paint_attribution
command line arguments when launching Chrome.
For a website that wishes to enable this for all their visitors to see the impact, there will be an origin trial running from Chrome 139 which can be enabled by signing up for the trial and including a meta element with the origin trial token in the HTML or HTTP header. See the Get started with origin trials post for more information.
Site owners can choose to include the origin trial on their pages for all, or for just a subset of users. Be aware of the preceding implications section as to how this changes how your metrics may be reported, especially if enabling this origin trial for a large proportion of your users. Note that CrUX will continue to report the metrics in the existing manner regardless of this soft navigation setting so is not impacted by those implications. It should also be noted that origin trials are also limited to enabling experimental features on a maximum of 0.5% of all Chrome page loads as a median over 14 days, but this should only be an issue for very large sites.
How can I measure soft navigations?
Once the soft navigations experiment is enabled, the metrics will be reporting using the PerformanceObserver
API as per other metrics. However, there are some extra considerations that need to be taken into account for these metrics.
Report soft navigations
You can use a PerformanceObserver
to observe soft navigations. Following is an example code snippet that logs soft navigation entries to the console—including previous soft navigations on this page using the buffered
option:
const observer = new PerformanceObserver(console.log);
observer.observe({ type: "soft-navigation", buffered: true });
This can be used to finalize full-life page metrics for the previous navigation.
Report the metrics against the appropriate URL
As soft navigations can only be seen after they have occurred, some metrics will need to be finalized upon this event, and then reported for the previous URL, as the current URL will now reflect the updated URL for the new page.
The navigationId
attribute of the appropriate PerformanceEntry
can be used to tie the event back to the correct URL. This can be looked up with the PerformanceEntry
API:
const softNavEntry =
performance.getEntriesByType('soft-navigation').filter(
(entry) => entry.navigationId === navigationId
)[0];
const hardNavEntry = performance.getEntriesByType('navigation')[0];
const navEntry = softNavEntry || hardNavEntry;
const pageUrl = navEntry?.name;
This pageUrl
should be used to report the metrics against the correct URL, rather than the current URL that they may have used in the past.
Getting the startTime
of soft navigations
The navigation start time can be obtained in a similar manner:
const softNavEntry =
performance.getEntriesByType('soft-navigation').filter(
(entry) => entry.navigationId === navigationId
)[0];
const hardNavEntry = performance.getEntriesByType('navigation')[0];
const navEntry = softNavEntry || hardNavEntry;
const startTime = navEntry?.startTime;
The startTime
is the time of the initial interaction (for example, a button click) that initiated the soft navigation.
All performance timings, including those for soft navigations, are reported as a time from the initial "hard" page navigation time. Therefore, the soft navigation start time is needed to baseline the soft navigation loading metric times (for example LCP), relative to this soft navigation time instead.
Measure Core Web Vitals per soft navigation
To include soft navigation metric entries, you need to include includeSoftNavigationObservations: true
in your performance observer's observe
call.
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('Layout Shift time:', entry);
}
}).observe({type: 'layout-shift', buffered: true, includeSoftNavigationObservations: true});
With the latest changes to the API, the includeSoftNavigationObservations
flag is no longer necessary and will likely be removed in the future but, for now, this explicit opt-in at the performance observer level is needed in addition to enabling the soft navigation feature in Chrome.
Timings will still be returned relative to the original "hard" navigation start time. So to calculate LCP for a soft navigation for example, you will need to take the interaction-contentful-paint
timing and subtract the appropriate soft navigation start time as detailed previously to get a timing relative to the soft navigation. For example, for LCP:
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const softNavEntry =
performance.getEntriesByType('soft-navigation').filter(
(navEntry) => navEntry.navigationId === entry.navigationId
)[0];
const hardNavEntry = performance.getEntriesByType('navigation')[0];
const navEntry = softNavEntry || hardNavEntry;
const startTime = navEntry?.startTime;
console.log('LCP time:', entry.startTime - startTime);
}
}).observe({type: 'interaction-contentful-paint', buffered: true, includeSoftNavigationObservations: true});
Some metrics have traditionally been measured throughout the life of the page: LCP, for example, can change until an interaction occurs. CLS and INP can be updated until the page is navigated away from. Therefore each "navigation" (including the original navigation) may need to finalize the previous page's metrics as each new soft navigation occurs. This means the initial "hard" navigation metrics may be finalised earlier that usual.
Similarly, when starting to measure the metrics for the new soft navigation of these long-lived metrics, metrics will need to be "reset" or "reinitialized" and treated as new metrics, with no memory of the values that were set for previous "pages".
How should content that remains the same between navigations be treated?
LCP for soft navigations (calculated from interaction-contentful-paint
) will measure new paints only. This can result in a different LCP, for example, from a cold load of that soft navigation to a soft load.
For example, take a page that includes a large banner image that is the LCP element, but the text beneath it changes with each soft navigation. The initial page load will flag the banner image as the LCP element and base the LCP timing on that. For subsequent soft navigations, the text beneath will be the largest element painted after the soft navigation and will be the new LCP element. However, if a new page is loaded with a deep link into the soft navigation URL, the banner image will be a new paint and therefore will be eligible to be considered as the LCP element.
As this example shows, the LCP element for the soft navigation can be reported differently depending on how the page is loaded—in the same way as loading a page with an anchor link further down the page can result in a different LCP element.
How to measure TTFB?
Time to First Byte (TTFB) for a conventional page load represents the time that the first bytes of the original request are returned.
For a soft navigation this is a more tricky question. Should we measure the first request made for the new page? What if all the content already exists in the app and there are no additional requests? What if that request is made in advance with a prefetch? What if a request unrelated to the soft navigation from a user perspective (for example, it's an analytics request)?
A simpler method is to report TTFB of 0 for soft navigations—in a similar manner as we recommend for back/forward cache restores. This is the method the web-vitals
library uses for soft navigations.
In the future, we may support more precise ways of knowing which request is the soft navigation's "navigation request" and will be able to have more precise TTFB measurements. But that's not part of the current implementation.
How to measure both old and new?
During this experiment, it is recommended to continue to measure your Core Web Vitals in the current manner, based on "hard" page navigations to match what CrUX will measure and report on as the official dataset of the Core Web Vitals initiative.
Soft navigations should be measured in addition to these to allow you to see how these might be measured in the future, and to give you the opportunity to provide feedback to the Chrome team about how this implementation works in practice. This will help you and the Chrome team to shape the API going forward.
For LCP, then means considering just largest-contentful-paint
entries for the old way, and both largest-contentful-paint
and interaction-contentful-paint
entries for the new way.
For CLS and INP, this means measuring these across the whole page lifecycle as is the case for the old way, and separately slicing the timeline by soft navigations to measure separate CLS and INP values for the new.
Use the web-vitals
library to measure Core Web Vitals for soft navigations
The easiest way to take account of all the nuances, is to use the web-vitals
JavaScript library which has experimental support for soft navigations in a separate soft-navs branch
(which is also available on npm and unpkg). This can be measured in the following way (replacing doTraditionalProcessing
and doSoftNavProcessing
as appropriate):
import {
onTTFB,
onFCP,
onLCP,
onCLS,
onINP,
} from 'https://unpkg.com/web-vitals@soft-navs/dist/web-vitals.js?module';
onTTFB(doTraditionalProcessing);
onFCP(doTraditionalProcessing);
onLCP(doTraditionalProcessing);
onCLS(doTraditionalProcessing);
onINP(doTraditionalProcessing);
onTTFB(doSoftNavProcessing, {reportSoftNavs: true});
onFCP(doSoftNavProcessing, {reportSoftNavs: true});
onLCP(doSoftNavProcessing, {reportSoftNavs: true});
onCLS(doSoftNavProcessing, {reportSoftNavs: true});
onINP(doSoftNavProcessing, {reportSoftNavs: true});
Ensure the metrics are reported against the correct URL as noted previously.
The web-vitals
library reports the following metrics for soft navigations:
Metric | Details |
---|---|
TTFB | Reported as 0. |
FCP | Only the first FCP for the page is reported. |
LCP | The time of the next largest contentful paint, relative to the soft navigation start time. Existing paints present from the previous navigation are not considered. Therefore, the LCP will be >= 0. As usual, this will be reported upon an interaction, or when the page is backgrounded, as only then can the LCP be finalized. |
CLS | The largest window of shifts between the navigation times. As usual, this when the page is backgrounded as only then can the CLS be finalized. A 0 value is reported if there are not shifts. |
INP | The INP between the navigation times. As usual this will be reported upon an interaction, or when the page is backgrounded as only then can the INP be finalized. A 0 value is not reported if there are not interactions. |
Will these changes become part of the Core Web Vitals measurements?
This soft navigation experiment is exactly that—an experiment. We want to evaluate the heuristics and see if they more accurately reflect the user experience before we make any decision on whether this will be integrated in the Core Web Vitals initiative. We are very excited at the possibility of this experiment, but cannot offer guarantees on whether or when this will replace the current measurements.
We value web developers' feedback on the experiment, the heuristics used, and whether you feel it more accurately reflects the experience. The soft navigation GitHub repository is the best place to provide that feedback, though individual bugs with Chrome's implementation of that should be raised in the Chrome issue tracker.
How will soft navigations be reported in CrUX?
How exactly soft navigations will be reported in CrUX, should this experiment be successful, is also still to be determined. It is not necessarily a given that they will be treated the same as current "hard" navigations are treated.
In some web pages, soft navigations are almost identical to full page loads as far as the user is concerned and the use of Single Page Application technology is just an implementation detail. In others, they may be more akin to a partial load of additional content.
Therefore, we may decide to report these soft navigations separately in CrUX, or perhaps weight them when calculating the Core Web Vitals for a given page or group of pages. We may also be able to entirely exclude partial load soft navigation, as the heuristic evolves.
The team is concentrating on the heuristic and technical implementation, which will allow us to judge the success of this experiment, so no decision has been made on these fronts.
Feedback
We are actively seeking feedback on this experiment at the following places:
- The soft navigations heuristics and standardization.
- Chrome implementation issues of those heuristics.
- General web vitals feedback at web-vitals-feedback@googlegroups.com.
Changelog
As this API is in experimentation, a number of changes are happening to it, more so than with stable APIs. You can see the Soft Navigation Heuristics Changelog for more details.
Conclusion
The soft navigations experiment is an exciting approach to how the Core Web Vitals initiative might evolve to measure a common pattern on the modern web that is missing from our metrics. While this experiment is in its early days yet—and there is much still to do—making the progress available so far to the broader web community to experiment with is an important step. Gathering the feedback from this experiment is another crucial part of the experiment, so we strongly encourage those interested in this development to use this opportunity to help shape the API to ensure it is representative of what we hope to be able to measure with this.
Acknowledgements
Thumbnail image by Jordan Madrid on Unsplash
This work is a continuation of work first started by Yoav Weiss when he was at Google. We thank Yoav for his efforts on this API.