skip to content

During a page load the browser emits several Largest Contentful Paint candidates. When does it stop updating the LCP value, and how can that make a slow page's field LCP look better than the experience actually feels?

level: middleimportance: should knowfreq 45%

answer

  1. a stream of candidates, last one wins
  2. the user ends the measurement
  3. impatience truncates the number
  4. hidden pages stop reporting entirely
  5. report on hidden, not on load

basics

~20 s

The browser reports a new LCP candidate each time something larger paints and stops looking once the user first interacts — a tap, click, scroll or key press — or once the page is backgrounded. An impatient user who scrolls early freezes the value at whatever had painted by then.

solid answer

~50 s

LCP is not measured once; the browser emits a candidate every time a larger content element paints, and the last one standing wins. Measurement ends at the first user interaction — a tap, click, scroll or key press — and also when the page is hidden, for example because it was opened in a background tab. The rationale is that after the user has engaged, later paints are no longer "the page loading". The side effect is a survivorship bias in field data: on a genuinely slow page, users scroll or click while waiting, which cuts measurement short, so the reported value is whatever small candidate had painted at that instant rather than the hero they were waiting for. Pages that never paint anything before the first interaction report no LCP at all and drop out of the dataset. This is also why RUM libraries report LCP on `visibilitychange`/`pagehide` rather than at the `load` event — until then the value is provisional.

go deeper

for a junior

Remember that the browser reports several LCP candidates as a page loads and that the last one before the user interacts is the value that counts.

for a middle

Explain both stopping conditions — first interaction and the page being hidden — and why a candidate is only emitted when something larger paints, not on every paint.

for a senior

Show the diagnostic instinct: record the winning element alongside the number, and treat a page whose LCP element varies across loads as evidence that impatient users are truncating the measurement.

for a principal

Own the reporting policy — collect on hidden rather than on load, keep the winning element in your schema, and be able to explain to stakeholders why a green field LCP is not proof that nobody is waiting.

## LCP is a stream, not a reading The naive mental model is that the browser waits for the page to settle and then announces the LCP. It does not. Every time a content element paints that is larger than the current largest, a new `largest-contentful-paint` entry is emitted. During a normal load you might see three or four: a heading, then a nav block, then the hero image. Each entry supersedes the last. The **final entry before measurement stops** is the page's LCP. This has an immediate practical consequence: any code that reads LCP at a fixed moment — on `DOMContentLoaded`, on `load`, after a timeout — can read a value that is still provisional. Candidates routinely arrive after the `load` event, especially on pages that render their main content with JavaScript. ## What stops measurement Two things end the stream: **First user interaction.** A tap, click, scroll or key press ends candidate reporting. The reasoning is definitional: LCP is a *loading* metric, meant to capture how long the user waited before the page looked ready. Once they have started using the page, whatever paints afterwards is a response to their input, not part of the initial load — and that territory belongs to the responsiveness metric instead. **The page being hidden.** If the page is backgrounded — opened in a background tab, or the user switches away — paint stops mattering and measurement ends. A link opened into a background tab and read a minute later will not report a realistic LCP, which is why field libraries discard or specially handle loads that were hidden before the first paint. ## The survivorship bias this creates Here is the uncomfortable part. Interaction ends measurement, and slow pages are exactly the pages users interact with early — they scroll to see if anything is there, they click the back button, they tap the thing that has already appeared. So on the slowest loads, measurement is cut short and the reported LCP is the small candidate that happened to be on screen at that instant. The metric is therefore biased *optimistically* on bad loads, and the bias is strongest precisely where you would most want the truth. Three symptoms follow: - Field LCP that looks acceptable while users report a slow hero. - An LCP element reported as a headline or a nav block on some loads and the hero image on others, from the same URL. - A distribution with a suspicious cluster of very fast values that correspond to abandoned or barely-viewed loads. A related case is the load where *nothing* has painted before the user gives up and leaves. There is no candidate at all, so no LCP is reported, and that page view — the worst one you had — is simply absent from the data. ## Diagnosing it The defence is to look at the element, not only the number. If you record which element won alongside the timestamp, a page whose LCP element varies wildly between loads is telling you that measurement is being truncated, not that the layout is unstable. Comparing the LCP timestamp against the time of first interaction (which the responsiveness instrumentation already gives you) makes it explicit: loads where those two are close together are loads where the value was cut off. It is also why a lab run and a field number diverge in a specific direction here. The lab robot never gets impatient; it lets the page finish and records the true largest paint. Real users truncate. That difference is structural, not noise. ## Reporting it correctly The rule that falls out of all this: **report LCP when the page is hidden or unloaded, not when it loads**. Practically that means listening for `visibilitychange` to `hidden` and for `pagehide`, and sending the last candidate seen at that point. Reporting at the `load` event under-counts late candidates; reporting on a timer picks an arbitrary moment. Every mature RUM library does the `visibilitychange` version, and if you hand-roll LCP collection and skip it, your numbers will be quietly wrong in a way no test will catch. ```js let latest; new PerformanceObserver((list) => { latest = list.getEntries().at(-1); }) .observe({ type: 'largest-contentful-paint', buffered: true }); addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden' && latest) send(latest.startTime); }, { once: true }); ``` The broader lesson is that LCP measures the *user's* wait, not the page's timeline, and a user who stopped waiting stops the clock.

  • Why do real-user monitoring libraries report LCP on visibilitychange rather than at the load event?
    Because candidates keep arriving after `load` — client-rendered heroes and late images routinely paint afterwards — so a value read at `load` is provisional and biased low. The value is only final when measurement ends, which is when the page is hidden or unloaded, so that is the moment to send it.
  • A user clicks a button 300ms into the load. What LCP does that page view report?
    Whatever the largest candidate painted in those 300ms was — very likely a small heading or nav block, and possibly nothing at all, in which case the load contributes no LCP. It will look like a superb result in your data despite the user having clicked into a mostly empty page.
  • Does this truncation mean you should distrust field LCP generally?
    No — it means you should read it alongside the winning element and the abandonment rate. The bias runs one way, optimistic, and only on bad loads. Field LCP is still the best signal you have for real conditions; you just cannot conclude "the hero is fast" from a good number without checking that the hero is what was measured.

saying these in an interview costs you the question

  • Thinks LCP is finalized at the load event
  • Believes scrolling has no effect on LCP measurement
  • Assumes every page view contributes an LCP value
  • Says a good field LCP proves the hero image rendered quickly
  • Claims a later, smaller paint can replace the current candidate

context