Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first-contentful-paint value can be reported as greater than the largest-contentful-paint value #260

Closed
vanderhoop opened this issue Sep 10, 2022 · 9 comments

Comments

@vanderhoop
Copy link
Contributor

vanderhoop commented Sep 10, 2022

Hey folks,
This may be an upstream (chromium) issue, but I wanted to start here, as there is a precedent of handling browser inconsistencies by ignoring/dropping values in the web-vitals library (see #187 and #147).

Expected behavior:

(Assuming what constitutes a contentful paint is consistent between first-contentful-paint and largest-contentful-paint, then) the first-contentful-paint entry's value should always be less than or equal to the largest-contentful-paint entry's `value.

Observed behavior:

Browser: Chrome 105.0.5195.102 (HTTP caching enabled, both incognito and non)
Mac OS: 12.5
Macbook Pro 2021 (M1 Pro chip)

The first-contentful-paint's value can be greater than the largest-contentful-paint's value. See photo below.

fcp_value_higher_than_lcp_value


Is the observed behavior reproducible?:

Yes. When HTTP caching is enabled, I'm able to reproduce consistently. If the observed behavior is unexpected and it'd be helpful to have a repro repo, let me know and I'll cook one up.

@vanderhoop
Copy link
Contributor Author

vanderhoop commented Sep 10, 2022

One thing to note in the photo above is that the LCP's attribution cites an elementRenderDelay of 72ms (the LCP is an image). This is a stab in the dark (I haven't peeped the source to see how elementRenderDelay is calculated), but maybe that value needs to be added to the currently reported value to determine the real paint timing for the user?

@vanderhoop vanderhoop changed the title first-contentful-paint value is greater than the largest-contentful-paint value first-contentful-paint value can be reported as greater than the largest-contentful-paint value Sep 10, 2022
@philipwalton
Copy link
Member

Hey @vanderhoop, thanks for the detailed issue.

I believe what's going on here (based on your screenshot) is that your LCP value is not entirely accurate since the image resource for your LCP element is loaded from a different origin that isn't setting the Timing-Allow-Origin header to expose detailed timing data. (See: loadTime vs. renderTime.)

Unfortunately there's not a lot this library can do about this, as it's a limitation of the web API.

If you control the surf-station.com domain, then an easy fix would be to set this header, otherwise just know that your data will be slightly off. As you can see from this case, though, the different is often not much (~3ms in your case), and loadTime values are still quite useful in optimizing LCP values.

@vanderhoop
Copy link
Contributor Author

I believe what's going on here (based on your screenshot) is that your LCP value is not entirely accurate since the image resource for your LCP element is loaded from a different origin that isn't setting the Timing-Allow-Origin header to expose detailed timing data.

Ahhh, interesting, so I swapped out an image resource with a cloudinary image where Timing-Allow-Origin is *, and with caching enabled the fcp/lcp values indeed coalesced, so you're correct on that front.

That said, it still feels like a bug to me that LCP could ever be lower than FCP in any scenario, regardless of image resource timings, and maybe that bug is at the browser/performance-entry-dispatch level. Maybe @noamr could chime in as to whether my gut is right here, or if I'm overlooking something?

@philipwalton
Copy link
Member

That said, it still feels like a bug to me that LCP could ever be lower than FCP in any scenario, regardless of image resource timings

This issue has been discussed in the WebPerf Working Group in the past. I'd take a look at this LCP spec issue and feel free to add any use cases you have that you feel would be relevant to the discussion.

@philipwalton
Copy link
Member

Also, on a completely separate note, I have a question about your screenshot: how is it that your LCP metric object contains attribution but your FCP method object doesn't? Are you importing those functions from different builds?

@vanderhoop
Copy link
Contributor Author

This issue has been discussed in the WebPerf Working Group in the past. I'd take a look at w3c/largest-contentful-paint#91 (comment) and feel free to add any use cases you have that you feel would be relevant to the discussion.

Perfect! I'll close this issue and head over there.

Also, on a completely separate note, I have a question about your screenshot: how is it that your LCP metric object contains attribution but your FCP method object doesn't? Are you importing those functions from different builds?

Yup, that's exactly it. Was trying to keep the bundle as small as possible, and the attribution data from LCP was the most immediately helpful.

@vanderhoop
Copy link
Contributor Author

Closing, as the issue appears to be upstream. Shifting to w3c/largest-contentful-paint#91

@philipwalton
Copy link
Member

Yup, that's exactly it. Was trying to keep the bundle as small as possible, and the attribution data from LCP was the most immediately helpful.

I believe this will actually result in a larger bundle. The "standard" and "attribution" builds are both full builds, meaning all of the code for each build is bundled in it's own file. If you're importing both then I suspect you're getting duplicate versions of much of the logic (internal modules shared by many functions), even after tree shaking.

This isn't documented (because it's complicated) but if you really want to only use attribution for some functions but not others, then you can import the modules directly (assuming you're using a bundler that support package exports), since the full module graph is published to /dist and all of the public modules are available via exports.

In other words, you can do something like this:

import {onCLS} from 'web-vitals/onCLS.js';
import {onFID} from 'web-vitals/onFID.js';
import {onLCP} from 'web-vitals/attribution/onLCP.js';

// ...

But if you're going to do that then you'd also need to make sure and define window.__WEB_VITALS_POLYFILL__ to be false ... so like I said, it's complicated and it's probably easier to just accept the 200-300 bytes of extra bytes for the full attribution build.

In version 4, once the polyfill is deprecated, this kind of thing will hopefully be much easier...

@vanderhoop
Copy link
Contributor Author

vanderhoop commented Sep 13, 2022

Hey @philipwalton, appreciate the callout here. Just did a comparison, and simply importing all of the utilized functions (onTTFB, onFCP, and onLCP) from the attribution build saves around half a KiB before compression (compared to my baseline of selectively importing just the onLCP function from the attribution build), and doing the more involved import suggested above saves about 1.5 KiB before compression. And I'll save any bytes I can.

This isn't documented (because it's complicated)

The README is already somewhat expansive, so I understand not wanting to add additional complexity/callouts for what may be an uncommon use case. That said, maybe adding something to https://github.com/GoogleChrome/web-vitals#attribution-build like the following may make sense?

If you're only looking to import certain functions from the attribution build, follow the import guidance discussed here to limit bundle size.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants