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 ignores opacity: 0 in some situations #11603

Closed
tannerhodges opened this issue Oct 28, 2020 · 13 comments
Closed

First Contentful Paint ignores opacity: 0 in some situations #11603

tannerhodges opened this issue Oct 28, 2020 · 13 comments

Comments

@tannerhodges
Copy link

Provide the steps to reproduce

  1. Run LH on https://demo.tannerhodges.com/vitals/fcp/opacity.html.
  2. Compare to https://demo.tannerhodges.com/vitals/fcp/index.html.

👉 See source code: https://github.com/tannerhodges/core-web-vitals/tree/main/fcp

What is the current behavior?

Lighthouse reports that FCP is the same (and sometimes better) when the body is hidden with opacity: 0 and revealed later during page load than when that style is removed and the page renders as quickly as possible.

For example, compare these WebPageTest filmstrips for the files above:

opacity.html index.html
FCP reports times faster when the body has opacity: 0 FCP reports slower times when the body renders as soon as possible
WebPageTest video for opacity.html View WebPageTest video for index.html
WebPageTest summary for opacity.html WebPageTest summary for index.html

Lighthouse correctly reports that LCP was faster in index.html, but incorrectly reports that FCP was slower. (Apparently, opacity.html rendered even sooner than the page appears in either video… 👀)

This is unexpected, and the effect grows in larger projects with similar code patterns.

In some cases, removing opacity: 0 from the body can increase FCP by as much as 1 second or more—even though the page is actually rendering much faster. 👇

Example where FCP reports ≈1s worse, even thought the page renders ≈2s faster

Unfortunately I can't share the code for this particular project but hopefully the reduced test case replicates the same issues well enough to debug what's going on.

What is the expected behavior?

  • Per the Paint Timing spec, elements with opacity: 0 should not be considered paintable.
  • First Contentful Paint should fire later in opacity.html than in index.html.
  • In these examples, FCP and LCP should both match since the h1 is the first & largest contentful element to render on the page.

Environment Information

  • Affected Channels: CLI, DevTools, and others. (I've confirmed the same result in PageSpeed Insights and WebPageTest.)
  • Lighthouse version: 6.4.1
  • Chrome version: 86.0.4240.111
  • Node.js version: 14.15.0
  • Operating System: macOS 10.15.7

Related issues

I'm still trying to get my head around Lighthouse's internals / which issues are actually relevant, but here are some I across while researching this that might (?) help:

I've also experienced similar issues with CLS ignoring opacity: 0, so in case the topic of paintable elements overlaps with FCP here are those issues:


P.S. At this point I'm not sure whether this should be considered a Lighthouse bug or a Chrome bug. Appreciate any insight y'all have as to what's going on here / next steps I should take.

Let me know if there's any additional information I can provide.

🙏 Thanks for the help.

@patrickhulce
Copy link
Collaborator

patrickhulce commented Oct 28, 2020

Thanks for filing with such great detail @tannerhodges! 😃

Many different issues at play here, some of which have satisfying answers and others less so. I can't spend any more time on this really based on what I saw unfortunately, but I'm flushing what I did find.

Per the Paint Timing spec, elements with opacity: 0 should not be considered paintable.

I haven't seen or been able to reproduce locally a case where FCP is marked before the opacity 0 flips, from my investigation the difference in metric times appears to come from the effect it has on paint scheduling (pushing Chrome to consider pushing a frame earlier, see below). If you find a minimal repro where there are not any other paint-blocking resources or tasks and FCP gets marked on a frame body is opacity 0, we can resume investigating here :)

First Contentful Paint should fire later in opacity.html than in index.html.

While when isolated this makes sense, there are too many competing factors at play in these examples that affect paint scheduling (multiple other unrelated render-affecting resources), such that I found no statistical significance to "opacity.html is occurring earlier than index.html" (40 runs locally). They appear to be the same on a macro level, which suggests to me that the opacity: 0 is not what is blocking paint most of the time here. Additionally, all examples I tried that isolate the opacity change, couldn't demonstrate this fact either.

The most likely explanation I can think of here is that by explicitly scheduling a body opacity change, Chrome considers that moment more important to ship a frame when it would otherwise wait for the webfont/script/etc that is currently being downloaded to finish which is why you occasionally see it taking longer in that case. In general, paint scheduling and coalescing isn't a deterministic or specable thing though and user agents are free to group multiple paints together if they think it will conserve resources or provide a more fluid experience.

In these examples, FCP and LCP should both match since the h1 is the first & largest contentful element to render on the page.

This is "expected" behavior in the sense that it's working as intended according to the spec, even though it is definitely a bummer that they differ :)

The simplest reproduction of the same concept at play here with the FCP/LCP discrepancy is loading http://melodic-class.glitch.me/fcp-opacity-0.html in the Performance tab in DevTools where you can see that FCP is marked at at time when no content is shipped at all and that LCP waits for the web font to finish downloading.

image

FCP does not wait for text to be visible in user sense of the word (and can therefore pick an invisible looking frame), LCP does wait for text to be visible (and won't pick the invisible text frame) (even under font-display: swap, browser will still give a very brief blocking period for the text to be painted if web font is inflight). I agree this isn't necessarily as useful from a developer perspective, so might be worth filing an issue against the spec with that minimal glitch example or see if it's already been brought up before.

@tannerhodges
Copy link
Author

Interesting!

Yeah, I was definitely under the impression that FCP was (is?) intended to measure paint timing in the user sense of the word. This gives me a much, much better sense of what's happening and some of the results I'm seeing.

Really appreciate you taking the time to share such a thorough answer.

❓ Questions:

  1. Where can I learn more about paint scheduling, render-affecting resources, and how/when/why Chrome decides to ship frames?
  2. In Dev Tools, how can I find the opacity "flip" in my Performance timeline? (I.e., how can I distinguish the time it was scheduled vs the time it actually rendered?)
  3. You mentioned "paint scheduling and coalescing isn't a deterministic or specable thing…" How could I frame my issue in a way that might make it more specable?

Any quick links/tips would be fantastic.

In the meantime, I'll try to whittle this down a bit more and follow-up again by the end of next week.

Thanks again.

@patrickhulce
Copy link
Collaborator

Where can I learn more about paint scheduling, render-affecting resources, and how/when/why Chrome decides to ship frames?

Some of those are easier to learn about than others :) I'm not quite sure where your starting points is, but from easiest to hardest...

Article (Resource priorities) / Article (rendering path) / List of Deep Technical Docs & Presentations (Graphics scheduling)

In Dev Tools, how can I find the opacity "flip" in my Performance timeline? (I.e., how can I distinguish the time it was scheduled vs the time it actually rendered?)

I knew which task it occurred in my repro simply because the page was very simple. In a complicated app, I would add a performance.mark to more readily see it in a Performance panel recording.

You mentioned "paint scheduling and coalescing isn't a deterministic or specable thing…" How could I frame my issue in a way that might make it more specable?

I'm not 100% sure what you mean by this, but I'm not sure you'll be able to :/

An example of how I would discuss the invisible text issue on the FCP spec would be something like "I wonder if we could consider using the first paint definition of LCP to wait for web fonts in FCP to avoid invisible text." Unfortunately though, it looks like FCP is explicitly spec'd to avoid waiting for webfonts so a discussion on this seems to have been settled and it's not just an oversight where an issue might solve it.

@tannerhodges
Copy link
Author

tannerhodges commented Nov 19, 2020

Hey @patrickhulce, thanks for these resources! They've been super helpful.

I've updated the demo: no fonts, no JS, just opacity or not (plus a new visibility example for comparison).

👀 Now I think this may actually be a Chrome bug…

Sanity check me here: am I right that this Opacity example should not return any FCP at all?

Screen Shot 2020-11-19 at 12 14 36 AM

For reference, here's a copy of the HTML/CSS…
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">

  <title>👨‍🔬 FCP Test - Opacity</title>
  <meta name="description" content="">
  <meta name="robots" content="noindex, nofollow">

  <!-- Styles -->
  <style>
    html {
      box-sizing: border-box;
      opacity: 0; /* 🚨 RED ALERT 🚨 */
    }

    *,
    ::after,
    ::before {
      box-sizing: inherit;
    }

    .container {
      padding-right: 1rem;
      padding-left: 1rem;
    }

    .scrollable {
      display: flex;
      overflow: auto;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>Testing First Contentful Paint</h1>

    <svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg">
      <circle cx="21.5" cy="21.5" r="16.5" />
    </svg>

    <ul class="scrollable">
      <li>
        <img alt="" src="./dist/images/188x17.png">
      </li>
      <li>
        <img alt="" src="./dist/images/114x21.png">
      </li>
    </ul>
  </div>
</body>
</html>

@tannerhodges
Copy link
Author

It seems like this only happens when you have a combination of…

  • SVG (maybe because of the parser switching?)
  • Flexbox.
  • Overflow scrolling containers.

If I remove/change any of those things from the demo then FCP disappears (as it should).

In particular, I think something about overflow scrolling containers is affecting whether FCP is reported for opacity: 0. When I run the page through PageSpeed Insights it reports an FCP on mobile but errors on desktop

The only difference, as far as I can tell, is whether the list content overflows and makes the container scrollable.

❌ Mobile ✅ Desktop

@patrickhulce
Copy link
Collaborator

Thanks very much for that minimal repro and additional investigation @tannerhodges! This is fascinating, to make things even more confusing, I copied the exact source of your page onto another domain with photos replaced and that produces the expected behavior (NO_FCP) while yours still gives an FCP... 🤯 Can you replicate the same result?

Definitely a Chrome bug, but not even one that makes any sense! I'll poke around a bit to see if I can find the difference in the hosting, if the specific image matters, etc.

@patrickhulce
Copy link
Collaborator

Wow, OK found the issue and filed https://bugs.chromium.org/p/chromium/issues/detail?id=1151054. Amazing work @tannerhodges hopefully we'll get a fix in Chromium soon :)

@tannerhodges
Copy link
Author

tannerhodges commented Nov 20, 2020

🙌 Fantastic!

I'm so relieved, I can finally take down my conspiracy wall.

FCP does not exist

Thank you again @patrickhulce for your feedback, resources, and help digging into this.

Excited to see it on its way to getting fixed.

@paulirish
Copy link
Member

CLS w/ opacity:0 is changing: https://chromium-review.googlesource.com/c/chromium/src/+/2591907

not FCP, but worth pointing out...

@midzer
Copy link
Contributor

midzer commented Apr 27, 2021

Since many LH versions I am experiencing a LCP penalty for having images with opacity animation on page load. Is this intended?

Example: https://feuerwehr-eisolzried.de/

I am unsure whether this fits here. Didn't want to open a new issue...

@paulirish
Copy link
Member

@midzer tbh i'm not sure. check https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/speed/metrics_changelog/lcp.md and try again?


as for @tannerhodges's original report. it appears to be fixed as of m94. see https://bugs.chromium.org/p/chromium/issues/detail?id=1151054#c7

@midzer
Copy link
Contributor

midzer commented Dec 14, 2021

@paulirish Thanks for your message and link. Yeah, the issue is fixed now for me.

@tannerhodges
Copy link
Author

🙌 Fixed at last! Thanks all.

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

No branches or pull requests

5 participants