Here is the code for testing:

<!DOCTYPE html>
<html>
<head>
  <title>test LCP</title>
</head>
<body>
<h1>Title</h1>
<div></div>
  <script>
    setTimeout(()=>{

      document.querySelectorAll('div')[0].textContent = 'Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...Test LCP ...';
    }, 1000)
    
  </script>
</body>
</html>

It's an extremely simple page. There are an h1 tag and an empty div. The js will render some text in that div after some time.

With the web vitals extension output, the console log says:

LCP Twice console output

  • First LCP happens when is displayed;
  • Second happens when is rendered with the text.

I thought this situation (LCP log) could only happen once on a page. But it did happen twice. So how would CrUX log the value? The bigger one?

However, I also did a test by using a greater number in the setTimeout parameter, say 100*1000 (more than one minute). And the log did not output the second LCP. Which means there is somehow a time that browser would wait for. If the time is longer than some value, it will not log the second change.

And I did my test by using different numbers, so here is what I have got so far:

If the number is not a very big one, the second log will happen.

If the number is set to 60*1000 (one minute), the log once happened, and another time did not happen. (this is really weird)

If the number is set to somehow a big one, like 180*1000 (three minutes), it will still trigger.

However, I can't find any related official docs on this behavior.

Can somebody give me a hint about the standard?

1

There are 1 answers

3
Kaiido On BEST ANSWER

It is supposed to fire multiple times.

The entry point is in HTML's update the rendering, which will call mark paint timing at every painting frame, which itself will report largest contentful paint with all the newly loaded images and all the new elements containing Text nodes as candidate.
Then potentially add a largest contentful paint entry will check if the document's window still has both its has dispatched scroll event and has dispatched input event flags off, otherwise it will abort the report.

So the rule is not time based, but rather based on user input.
If the document's window received a scroll event with its isTrusted set (i.e. not a script generated one), or if the document's window received a pointerup following a pointerdown event, a click, or a keydown, or a mousedown event, then it won't report LCP anymore.

Note also that the intersectionRect that's used to determine which element is the biggest is cropped by the viewport rect (here). So if your new element is outside of the viewport it won't count as LCP. (Thanks to Barry Pollard for the note).

In below test you can see that as soon as you click in the iframe, the report is stopped. You may also notice that if you let it long enough, when the page starts overflowing, it will not report the new elements that have been cropped, but clicking on "Full page" (without interacting with the iframe) will make the new elements in screen to be reported.

// Please use your browser's console. StackSnippet's console's element
// would also participate in LCP candidates, which is confusing.
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log("LCP:", lastEntry.element);
});
observer.observe({ type: "largest-contentful-paint", buffered: true });

const test  = (size) => {
  const img = new Image(size, size);
  img.src = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png";
  document.body.append(img);
};
let size = 10;
test(size += 10);
const int = setInterval(() => test(size += 10), 1000);
document.querySelector("button").onclick = (evt) => clearInterval(int);
<button>Stop test</button>