Open hbpatel142 opened 3 years ago
Had the same question, the docs need some context for SPAs (or any app with history-based navigation)
For example, there should be a way to manually reset the values based on a trigger or event.
I had wondered whether subsequent page loads with an SPA are even (or should be) considered wrt web-vitals? Either way I'd be interested in seeing some guidance on how to handle web-vital measurement for SPAs
Unfortunately, accounting for SPA route transitions in Core Web Vitals metrics is not currently possible. There are a few primary reasons for this, both technical reasons and practical reasons.
The technical reason is that the Web APIs that measure page load specific Core Web Vitals metrics (LCP and FID) do not re-emit entries after a SPA route change occurs. For FID we could use the same polyfill we use in the case of a bfcache restore, but for LCP we could not, since route changes typically involve loading additional content, and polyfilling LCP to handle those cases would be near impossible (and it would certainly affect performance).
Also, as you point out, in the case of CLS we could reset the metric value to 0 (as we do for a bfcache restore), but this is where the practical issues come into play: what counts as an SPA route change?
history.pushState()
be considered a route change?history.replaceState()
?While I understand it's probably quite easy for you to determine what should be considered a "logical page load" for your specific app. Making that determination for any given web page is pretty much impossible.
Finally, and perhaps the most challenging of the practical issues is that both Google Chrome and Google Search are creating incentives around these metrics, which means it's absolutely critical that they can't be easily gamed.
If we consider every call to history.pushState()
to be a new page load, then developers would have a strong incentive to use that API, even in cases where a user wouldn't consider it a "logical page load".
For example, you could easily imagine some code like this:
// Create a new "page load" any time CLS gets close to crossing the 0.1 threshold...
If (cls > 0.09) {
history.pushState({...}, document.title, location += '&addendum=1)
}
Similarly, developers might try calling history.pushState()
during idle periods to increase the likelihood of LCP and FID being super fast.
Hopefully this helps explain why Core Web Vitals metrics currently don't consider SPA route transitions. But rest assured we are aware of this problem and looking into solutions. Here are two concrete examples:
Thanks for the quick response!
Above is really good explanation on why current route change identification (within SPA) alone is not sufficient for the web vitals that must not be gamed, but at the same time very critical that it's measured accurately, since this would impact page ranking in May 2021 unless the timeline is changed.
It is really great that the team is actively looking for the solution and the official documentations around SPA have started to surface recently.
Thanks for the response! I would just add that or part of that to the docs for the package.
Even if vitals don't apply, there are natural questions that get asked: "how does this apply to SPAs?" A few sentences would answer that cleanly.
FYI: we're working on some content related to SPAs for web.dev/vitals. Once that's published we can link to it from this repo.
Wow, I've spent months pondering why google search console flags so many of our SPA's pages as having poor CLS but lab data (lighthouse) and performance traces (Chrome dev tools) suggest it's very good.
On first load we generally see web-vitals
report 0
, but with each SPA navigation we see our CLS creep up and up which is why we're seeing such crazy (and unfair?) values; all we're really garnering from CLS is how long somebody is on the site for 🤦🏻♂️
I think the layout shifts which are being accumulated across SPA navigations are those caused by a successful network response after navigation because, like many SPAs, on click we show a blank screen with a loading indicator while fetching the data, and then when the data arrives we render it to screen causing a layout -- if the page data being navigated to is already in memory then I've observed the CLS isn't reported, I guess because the layout is the direct result of a click handler -- https://www.paradeworld.com/
In the meantime we're going to only report the first CLS value reported by web-vitals
, but it's terrifying to think google search is going to factor in CLS over the entire session and penalise SPAs offering a decent user experience?
Hey @richardscarrott, see the post on how we plan to update CLS that I mentioned above to address your concern about how the value just keeps increasing the longer the user is on the page.
As for the the technique of showing a loading spinner and then filling in the content once the network request completes, this may still be counted as layout shifts—if it's the case that elements on the page are shifting as new content is being added.
I'd recommend updating your SPA transition logic to either:
dipslay:none
, visibility:hidden
, or opacity:0
) and then only show it once it's all available.Hi @philipwalton thanks for the advice.
I had a bit of a think about your last point re: hiding the content while it's loading and realised I had initially thought the layout shift was merely counting the layout of the new content rendered to screen but of course it's our footer which get's shifted further down the page.
It wasn't immediately obvious to me because we do render the footer offscreen while loading by reserving the height of the viewport; this was done specifically to prevent the footer flashing into view. e.g.
And I guess, although unlikely imo, a user could scroll the footer into view during the loading period and then genuinely experience a layout shift. It seems layout shifts don't discriminate against things inside vs outside the viewport 🤔.
I tested with the footer hidden while loading and as you've suggested this solves the issue so we're seeing almost 0 CLS reports during the page lifecycle.
Of course, we still have the issue of the now rarer and slight layout shifts accumulating over the SPA session but I'll give that article another read and look forward to hearing about improvements on this.
UPDATE: So I had a proper read of the CLS docs and of course it does attempt to only consider layout shifts which are in the viewport, however it seems to get this wrong with our footer. Adding margin-top: 21px
to our footer (20px doesn't quite cut it on all viewport sizes I tested 🤷♂️) prevents the CLS event from firing on SPA navigations; obv we're then betting on the user not scrolling the footer into view while loading, but will see how this goes (Side note, I feel like I'm gaming the metric a little here).
UPDATE2: So there obviously had to be a reason it was 20px specifically and I think it's because we have a negative margin on a child element in our footer to compensate for some third party styles, so although visually it didn't appear to be in the viewport it's bounding box did creep in by 20px.
I am just getting started with measuring of CLS and LCP for my React based SPA and came across web vitals module and this thread. I just played around with the api getCLS and getLCP for my SPA by including a call in one of my pages when the page component is loaded by JS.But I do not see CLS score or LCP score printed on console.LCP gets printed once after I interact with the page, like a simple click on the UI and no amount of route changes prints it again. CLS never gets logged at all. Is the conclusion of this particular thread that for SPAs, web-vitals module may not work as expected?Are there any steps I can perform on my SPA to have getLCP and getCLS work properly?
@Midhun-Carousell
LCP gets printed once after I interact with the page, like a simple click on the UI and no amount of route changes prints it again.
This is expected. LCP is only measured at page load, and this is intentional for the reasons I mentioned above. Also, the reason you're seeing LCP logged to the console after you interact is because that is the point when LCP observation stop waiting to see if a larger element gets added to the DOM (we can't log the value before that point).
CLS never gets logged at all.
CLS is only logged when the page is backgrounded or unloaded, so if you switch tabs you should see it logged. You'll also see it logged if you check the preserve log option in devtools and reload the page (it will log CLS for the previous page).
For both LCP and CLS, if you want to log the metric value every time it changes, you can do that with the reportAllChanges
option.
Is the conclusion of this particular thread that for SPAs, web-vitals module may not work as expected?
I suppose it depends on what you consider "expected", but based on what you've described, it sounds like it's working exactly as intended.
If you were expecting all the Web Vitals metrics to re-emit after a route change in an SPA, that is not expected to happen.
@richardscarrott thanks for the update.
UPDATE2: So there obviously had to be a reason it was 20px specifically and I think it's because we have a negative margin on a child element in our footer to compensate for some third party styles, so although visually it didn't appear to be in the viewport it's bounding box did creep in by 20px.
This library is just collecting what the underlying Layout Instability API is reporting. If you think this behavior is confusing or wrong, I'd recommend filing an issue on the spec highlighting your use case, and it will be discussed there.
@philipwalton even after the new update I can still see the same page id when I navigate btw pages in a SPA. This means that the cumulative CLS is summed to the initial page.
On a flow: search page => category page all the CLSs have the same id. Is this expected?
Is this expected?
Yes, this is expected. Until we have more reliable ways to detect user-initiated SPA transitions (or a dedicated web APIs to measure them) then Core Web Vitals metrics will need to be attributed to the URL was that present at the time the page was loaded.
We recognize that this makes debugging quite awkward in cases like the one you described: where the problematic layout shifts occur after an SPA transition to a new URL.
If you're measuring your Core Web Vitals with an analytics tool, we recommend collecting both the URL at the time of the shift and the URL that the page was originally loaded at for debugging purposes.
Just to add to this conversation, this is an email I had sent previously to the Chrome Speed team in Feb 2021:
I would like to discuss with you two scenarios, both Ajax-related, that have been affecting my website's CLS score.
.
Scenario 1:
My website / PWA uses Ajax navigation, which keeps Header/Footer and triggers pushState(), every time someone clicks a link to navigate. When a navigation happens, the URL changes, and the contents (except Header and Footer) are replaced with the new page.
I noticed that the CLS score increases because the Footer is pushed up or down, when the main contents are updated. This makes CLS increasing "forever", while my users are on my website, causing a Red/Bad CLS on my domain overall.
Any idea how I can go around that, and reset CLS score every time a user navigates?
I'd say that if, within approx. 3~5 seconds:
- User clicked a link/button
- pushState() happened
- An HTTP request happened
- DOM was updated
Then CLS could be reset, or at least "ignored".
.
Scenario 2:
Think about Comments on a Post. And users can reply to the comments (nested replies). We do something similar to Facebook/Instagram, where we show the last 3 nested comments, but you can "View X more comments". Clicking on that button, we do an Ajax request to load the previous 10 comments (between the Parent comment and the already-loaded nested comments). Since this is an async behavior, it causes CLS issue by default (as it moves the DOM below, to load the new comments above other comments in the page).
No problem, we can reserve some space on the screen as soon as the user clicks. The problem is that the system needs to know in advance how much space those comments (to be loaded) need... otherwise, if we reserve too little space, CLS will be an issue after the comments load; but if we reserve too much space, then there will be empty/blank space in between the new comments and the old comments. If we reserve some space and then remove the excess after the comments load, CLS is an issue again.
How can we make "Google/CLS" happy here? I hope you understand my position here. Our users have never complained about "layout shifting" here. It's a user action, but because it's an async request, it causes CLS in Google's view.
.
I believe a solution similar to the suggested in Scenario 1 might help, although in this case we wouldn't do pushState().
I think I'm mostly curious about whether we should even be tracking web vitals one page transitions within SPAs. When google uses vitals scores, is it accounting for internal navigation within a site, or is it simply comparing the scores from the initial page load of each individual page as an entry point? Clearly, the entry point load is always going to have worse scores than any internal transition, as there is some framework boilerplate that you need on the initial load of a page that won't happen if you transition to that same page after initially entering the app on another page (ie, scores for going directly to example.com/about would be worse than scores computed transitioning to /about after already loading the example.com homepage, right?).
Correct me if I'm wrong, but my understanding is that google measures the vitals on the page from the entry point perspective (ie someone clicks a link from google directly to a page) and thus measuring metrics of internal page transitions are just going to conflate your aggregations to a much more favorable number than what google is actually ranking you for, no?
To provide an update to everyone, we recently published a post on web.dev with answers to common questions about how SPA architectures affect Core Web Vitals: https://web.dev/vitals-spa-faq/
The post includes a section re: what is Google doing to address the SPA issue with some specific plans. Once there's more progress on those proposals I'll update this library to incorporate any changes here.
Finally, and perhaps the most challenging of the practical issues is that both Google Chrome and Google Search are creating incentives around these metrics, which means it's absolutely critical that they can't be easily gamed.
I think the development of Web Vitals should depend on the need for web performance optimization and not be constrained by Google search, although Web Vitals is dominated by Google. I'm sorry if i caused offense.
I do agree with @zhouqicf
I must say core web vital is cool and can help to measure whether the website is cool enough or not But because of the limitation in calculation on SPA, we may need to give up the SPA and change back to using server-side route (MPA) It's quite nonsense that because of the SEO, we need to make the user experience worse.
Please note there is experimental support of this in the soft-navs
branch as detailed here: https://developer.chrome.com/blog/soft-navigations-experiment/
I used the web Vitals extension Google browser plugin to observe SPA single page routing switching and found that it can collect changes in CLS and LCP indicators, but calling the onCLS and onLCP methods with web Vitals does not work
Yes we have not released a version of the extension with this experimental branch yet because it is still experimental and subject to change.
Yes we have not released a version of the extension with this experimental branch yet because it is still experimental and subject to change.
I'm very sorry! I probably didn't express the issue clearly .
I installed the web Vitals plugin in Chrome browser; The name of the plugin is' web vitals extension'
the browser plugin link https://github.com/GoogleChrome/web-vitals-extension
The browser plugin can detect SPA router change values, why can't I use web Vitals in the code to obtain these change values .
Examples:
The web vitals extension always shows the current URL when the Heads Up Display (HUD) is opened. This has not changed and was always the case. Perhaps we should change this, to make it more obvious it's based on the initial page.
LCP for an SPA is measured across all the soft navigations. However LCP is finalised when a click happens, so normally you wouldn't see an LCP change (assuming you do a click to initiate the soft navigation?). However, if there was an automatic change in SPA route, without an interaction, then yes the LCP would still continue to take the latest one if it is bigger. Chrome will also measure this as a new LCP but report it back based on the initial URL.
The experimental branch, however, "resets" LCP so it is logged on the second and subsequent SPA page even if it is not bigger. So that's why it's different.
Coming here after I realize that INP and CLS are fired on window unload (and after reading web vitals faq)
Just want to add on our specific use case.
The specific issue for our specific use case:
/foo/bar/*
. LCP and FCP are emitted with trackingId
as /foo/bar*
./whatever/:title
.trackingId
as /whatever/:title
.trackingId
. Like many have said, would be great to see them fired as well on route (history?) change, that way we would be able to use the metrics for the same route to calculate our custom scoring system for that particular route. This is why it's ideal for all the metrics to get emitted in the same route.
Unfortunately, accounting for SPA route transitions in Core Web Vitals metrics is not currently possible. There are a few primary reasons for this, both technical reasons and practical reasons.
The technical reason is that the Web APIs that measure page load specific Core Web Vitals metrics (LCP and FID) do not re-emit entries after a SPA route change occurs. For FID we could use the same polyfill we use in the case of a bfcache restore, but for LCP we could not, since route changes typically involve loading additional content, and polyfilling LCP to handle those cases would be near impossible (and it would certainly affect performance).
Also, as you point out, in the case of CLS we could reset the metric value to 0 (as we do for a bfcache restore), but this is where the practical issues come into play: what counts as an SPA route change?
- Should every call to
history.pushState()
be considered a route change?- What about
history.replaceState()
?- What about apps that still use hash URLs for SPAs? (A lot of those still exist.)
While I understand it's probably quite easy for you to determine what should be considered a "logical page load" for your specific app. Making that determination for any given web page is pretty much impossible.
Finally, and perhaps the most challenging of the practical issues is that both Google Chrome and Google Search are creating incentives around these metrics, which means it's absolutely critical that they can't be easily gamed.
If we consider every call to
history.pushState()
to be a new page load, then developers would have a strong incentive to use that API, even in cases where a user wouldn't consider it a "logical page load".For example, you could easily imagine some code like this:
// Create a new "page load" any time CLS gets close to crossing the 0.1 threshold... If (cls > 0.09) { history.pushState({...}, document.title, location += '&addendum=1) }
Similarly, developers might try calling
history.pushState()
during idle periods to increase the likelihood of LCP and FID being super fast.Hopefully this helps explain why Core Web Vitals metrics currently don't consider SPA route transitions. But rest assured we are aware of this problem and looking into solutions. Here are two concrete examples:
- We plan to update CLS in the near future to be more fair to long-lived pages (including SPAs).
- We're working to spec a new version of the history API that might enable us to more accurately identify SPA route changes in the future.
@philipwalton Hi I was wondering is there any update on this? Is there any metrics that is accurate on SPA from the improvements? I'm particularly interested in FID and INP
@philipwalton Hi I was wondering is there any update on this? Is there any metrics that is accurate on SPA from the improvements? I'm particularly interested in FID and INP
This is the most recent update: https://developer.chrome.com/docs/web-platform/soft-navigations-experiment
@philipwalton thanks for you comments here, I understand why web-vitals package or chrome by itself can't identify what is a SPA page transition that should trigger a reset. Having said that, it would be amazing if the web-vitals package itself had an API that can be used when the website owner knows it's a SPA page transition and it's now time to reset cumulative metrics and re-emit FID on this page load.
I hope that makes sense to add to the existing package or create a separate package which will add this support. Thanks
In React Single page Application(SPA), First Input Delay and Largest Contentful Paint are only measured once on Initial Load. Additionally, Cumulative Layout Shift does not reset to 0 throughout the session. This means CLS could reach to very high value , if user continues to navigate though logical pages like homepage -> search -> Product Listing -> Product Page -> Basket -> Checkout and so on.
Based on current implementation, pageshow event resets CLS to 0 and it also captures FID and LCP for subsequent page loads.
In React Single Page Application, Could we consider route change event for the new logical page load and reset CLS to 0. This would also enable us to capture FID and LCP on every logical page of the application.
If Web Vital Metrics would be considered for page ranking, the proposed implementation would give React SPA fair comparison.