Largest contentful paint: how to measure it and fix the slowest part

Largest contentful paint is the render time of the biggest element in the viewport, and the target is 2.5 seconds or less. Here is how to find the element, split the four subparts, and fix the one that dominates.

Implementation7 min read1603 views
Largest contentful paint: how to measure it and fix the slowest part

Largest contentful paint is the moment the biggest thing in the viewport finishes rendering, and the target for it is 2.5 seconds or less. Google defines it precisely: LCP "reports the render time of the largest image, text block, or video visible in the viewport, relative to when the user first navigated to the page" (web.dev, read 2026-09-30). Miss that target and you have not lost a ranking factor. You have lost the first two seconds of a stranger's attention.

Read this first

You need a page you can load in a browser and permission to run a command line. Nothing else. Two chapters sit next to this one. If you are still deciding whether speed is worth your time, does page speed affect SEO settles that question and this chapter assumes the answer. If you have already measured a slow number and want to pull it programmatically instead of by hand, the PageSpeed Insights API is the machine-readable route. The measured side of the same problem, how often the blocking resources that delay LCP actually appear, is in render-blocking CSS across 26 homepages.

What largest contentful paint measures, and what counts as the big element

LCP is a single number with a narrow definition. The browser watches the viewport, records the largest element that has painted, and reports the time from the start of navigation to the moment that element finished rendering. The element can change as the page loads: a heading may be the largest thing first, then a hero image finishes and takes over. The metric reports the last of those candidates, because that is what the user actually waited for.

Only a few element types are eligible, and the list is deliberate rather than accidental:

  • An <img> element, using the first frame for animated content.
  • An <image> inside an <svg>.
  • A <video>, using its poster image or first frame, whichever comes first.
  • An element with a background image set through url(), not a CSS gradient.
  • A block-level element whose content is text.

Chromium also skips elements a reader would not call content: fully transparent elements, elements that fill the whole viewport and read as background, and low-entropy placeholder images. The size the browser compares is the size visible in the viewport, after clipping, and the smaller of the rendered or the intrinsic size for a resized image.

LCP is not the time your page finished loading. It is the time the one thing on screen that matters finished appearing.

Why one number hides four separate problems

A single LCP value tells you that the page is slow; it never tells you which part is slow. The useful move is to split it into four subparts that add up to the whole with no overlap. Every page can be broken down this way, and each subpart points at a different fix.

SubpartWhat it coversHealthy share
Time to first byteNavigation start until the first byte of HTML arrivesAbout 40%
Resource load delayEnd of TTFB until the LCP resource starts loadingUnder 10%
Resource load durationHow long the LCP resource itself takes to transferAbout 40%
Element render delayResource finished until the element is renderedUnder 10%

Those proportions are guidelines, not rules, and web.dev is explicit that they are only meaningful relative to each other, so turning them into absolute milliseconds is a mistake. The shape is the point: most of the time should be spent moving the HTML and the LCP resource, and any window where neither is moving is an opportunity.

The proportions also explain a trap. Shrinking an image can shorten the load-duration share while LCP does not move at all, because the time simply shifts into the render-delay share. A page that hides its hero behind a script will show exactly that behaviour: the bytes arrive sooner, and the picture still appears at the same second.

Do it in this order

Four steps. Each has a finishing condition you can check before moving on.

  1. Read the field number first, not the lab number. Real visitors are what the metric is for, and a lab run on a fast laptop is not them. Done when you can name your 75th-percentile LCP for mobile and desktop separately.
  2. Find the LCP element. You need to know whether the big thing is an image, a heading or a video before you can fix it. Done when you can paste the element's selector and its type.
  3. Break the value into the four subparts. Done when you can say which subpart dominates and point at the resource responsible.
  4. Fix the dominant subpart only. Change one thing, then re-measure. Done when the subpart moves and the total moves with it.

The deliverable: one snippet, one reading command, one fix table

Paste this into your page or run it from the console to see every LCP candidate and the element behind it. It is the same observer the field tools use, minus the bookkeeping.

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('LCP candidate:', entry.startTime, entry.element);
  }
}).observe({type: 'largest-contentful-paint', buffered: true});

The last line it prints is usually your LCP, but not always, and the API is honest about the difference. It keeps reporting candidates for pages opened in a background tab, which the metric ignores, and it does not report anything for pages restored from the back-forward cache, which the metric counts. The web-vitals library handles those cases for you and exposes the same number the Chrome User Experience Report uses.

On the command line, one call gives you a lab number and the LCP element in one pass:

npx lighthouse https://example.com \
  --only-audits=largest-contentful-paint,largest-contentful-paint-element \
  --output=json --output-path=lcp.json --quiet

Read it with the field number beside it, never instead of it. When the two disagree, trust the field data.

Once you know which subpart dominates, the fix is usually one of five changes:

SubpartTypical causeFix
Load delayHero added by script, or a lazy-load libraryPut the src in the HTML; never lazy-load the LCP image
Load delayBrowser does not know the image mattersAdd fetchpriority="high", or preload it
Render delayLarge stylesheet or synchronous script in the headInline or shrink the CSS, defer the script
Load durationOversized or wrong-format file, far from the userResize, switch to a modern format, serve from a closer edge
Time to first byteRedirect chains, uncached origin, far serverCut redirects, cache the HTML, move the origin closer

Two of those deserve a rule of their own. Never lazy-load the LCP image: lazy loading waits for layout to confirm the image is on screen, which is exactly the delay you are trying to remove. And set a high fetch priority on one image, not five, because priority spread across many requests stops meaning anything.

What we measured, and what it does not prove

We ran Lighthouse 12 once per site, mobile, on 2026-09-30. Two numbers came back, and neither flatters the people who wrote this chapter. Google's own LCP guide, the page that teaches this metric, scored 3.2 seconds. Our own homepage, querywin.com, scored 5.8 seconds. Both are above the 2.5-second target; ours is above the 4.0-second line that the documentation calls poor.

That single run is the whole sample, so read it as a snapshot, not a benchmark. A lab run is a simulation with a fixed device and throttled network, and it will not match what your visitors experience. It cannot tell you whether the real-world number is better or worse, and on our own site we have not compared the two yet. What it does show is the floor: if the page that explains LCP cannot pass its own test, a page that has never looked is probably not passing either.

Three ways this goes wrong

The first two waste a sprint. The third is the reason people give up on the metric.

  1. Optimising the wrong element. Teams shrink the first image in the markup, which is often not the LCP element. Measure until you have the actual element before touching a file.
  2. Measuring only in the lab. A green Lighthouse score on a laptop with a fast connection is not a green field number, and the field number is what the metric reports.
  3. Fixing one subpart and stopping. An optimisation that only shifts time from one subpart to another leaves the total unchanged. Re-measure after every change, or you will not know which kind you made.

Common questions

Is largest contentful paint a ranking factor?

It is part of Core Web Vitals, which Google lists as one input among many, and we would not present it as more than that. The defensible reason to fix it is that a page which paints its main content in under 2.5 seconds keeps more of the people who land on it.

What is a good LCP score?

2.5 seconds or less, measured at the 75th percentile of page visits, with mobile and desktop counted separately. Above 4.0 seconds is poor, and anything between needs improvement.

Should I lazy-load images to improve LCP?

Lazy-load the ones below the fold, never the LCP image. Lazy loading the biggest above-the-fold element adds the exact delay this metric measures.

Why does my LCP differ between tools?

Because lab tools and field data answer different questions. Lab tools run one simulated load, while field data aggregates real visits. When they disagree, the field number describes your users and the lab number only helps you explain it.

Next step

LCP tells you when the page was usable. The other half of the page experience is whether the layout stayed still while it loaded, and that is its own chapter. If you only take one thing from this one, take the loop: measure the element, split the four subparts, fix the largest, measure again. Doing that on the pages visitors actually land on is a large part of what QueryWin works on.

Part of the QueryWin handbook · Level 2