LCP: Finding the Element That Decides Your Score
LCP is the Core Web Vital most likely to be optimised by guessing. Someone decides the hero image is the problem, preloads it, and the number does not move — because the element that actually decided the score was something else.
The reason is in the specification: the LCP element is determined by an algorithm that runs during loading, and its answer can change.
The definition, and the four sub-parts
LCP is the render time of the largest image or text block visible in the viewport, measured from when the page began loading.
| Rating | LCP |
|---|---|
| Good | ≤ 2.5 s |
| Needs improvement | 2.5–4.0 s |
| Poor | > 4.0 s |
Assessed at the 75th percentile of page loads.
What makes this metric actionable is its decomposition into four non-overlapping phases that sum to the total:
| Sub-part | Covers |
|---|---|
| Time to first byte | Up to the first byte of the HTML response |
| Resource load delay | From TTFB to the moment the LCP resource starts loading |
| Resource load time | The duration of loading the LCP resource |
| Element render delay | From the resource being loaded to the element being rendered |
Two useful properties. First, for a text LCP element using a system font, the last two are effectively zero — there is no resource to fetch. That is the structural advantage of text over images, and it is why a text-heavy page can pass LCP with a slow server and an image-heavy page cannot.
Second, Google publishes rough proportions as a diagnostic starting point: around 40% TTFB, under 10% resource load delay, around 40% resource load time, and under 10% element render delay — labelled explicitly as guidelines rather than rules.
Use those proportions as a triage tool. If your TTFB is 60% of your LCP, you have a server or network problem and no amount of image optimisation will fix it. If resource load time dominates, you have a file-size problem.
The candidate can change
This is the property that makes LCP counter-intuitive, and the specification states it directly.
The algorithm tracks the largest element it has seen as a candidate, and whenever a new larger element is found, a new entry is created for it. So the reported LCP can move during the load — a hero image may be the candidate at 800 ms, then get superseded by a larger element that renders at 1,400 ms.
Two consequences:
Optimising the first candidate may not matter. If the element that ends up defining the score is one that renders later, work on the earlier one was wasted.
The algorithm stops on interaction. The spec states: “The algorithm terminates whenever scroll or input events occur, since those are likely to introduce new content into the website.” This is a deliberate design choice — it prevents an infinitely scrolling page from reporting an LCP that keeps receding.
There is one more property worth knowing, and it is genuinely strange: “Content that is removed is still considered by the algorithm.” An element that was the largest and was then removed from the DOM remains a valid LCP candidate. If you have a cookie banner or a full-screen loader that gets dismissed on load, it may be your LCP element — and it is not something you would optimise by shrinking the hero image.
What qualifies as a candidate
The eligible element types are documented:
<img>elements<image>elements inside SVG<video>poster images- Elements with a
background-image - Groups of text nodes — a paragraph counts, not just a single text node
The background-image entry surprises people. A CSS background is not an image element, it does not appear in the markup as a resource, and it is the classic case of an LCP element nobody identified. If a page has a hero section built with a CSS background, that is very likely the LCP element, and the fix is different from an <img> — you cannot set fetchpriority on a background image, and preloading it means preloading a CSS-declared URL.
Note the frame: the specification itself does not enumerate these types. The list comes from the API documentation, which is where the implementation contract is written.
Finding your actual LCP element
Do not guess. Three documented methods:
In the lab: the Lighthouse “Largest Contentful Paint element” audit names it, and DevTools highlights it on the page.
In the field-adjacent tooling: the Web Vitals extension reports which element was identified and prints the sub-part breakdown — the most useful single readout, because it tells you which phase to work on.
Programmatically:
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
console.log(last.element, last.startTime, last.url);
}).observe({ type: 'largest-contentful-paint', buffered: true });
Two caveats about reading those entries. For cross-origin images without Timing-Allow-Origin, the renderTime value is coarsened for privacy reasons, so startTime is the more reliable field. And note that a text LCP entry has no url at all — if you are logging entry.url and seeing undefined, you may be looking at a text candidate.
The two things that actually move it
For an image LCP: make the resource discoverable earlier and smaller. Preload it, give it fetchpriority="high", ensure it is not lazy-loaded, and serve it at a sensible size. Google’s documented recipe includes a preload link with the priority hint set, and the explicit warning that lazy-loading the LCP image is a pattern to avoid.
For everything: reduce TTFB. Since TTFB is a floor on LCP — the largest element cannot render before the first byte arrives — a slow server caps your best possible score. If TTFB is 800 ms, your LCP cannot be under 800 ms plus render time, no matter how well-optimised the image is.
The corollary, from the TTFB article: once TTFB is reasonable, stop optimising it. Below roughly half a second it stops being the binding constraint, and effort belongs on the resource.
Where the preload advice goes wrong
Preloading is the most over-applied fix in this area. Two failure modes:
Preloading the wrong element. If you preload an image that is not the LCP element, you have added a high-priority request that competes with the one that is. The page gets slower.
Preloading something the browser would have found anyway. For an <img> early in the HTML, the preload scanner already prioritises it. A manual preload adds nothing except a duplicate-invocation risk if the as or type values do not match the element exactly.
So the order is: identify the element, confirm it is a resource-backed LCP (not text, not a removed element), and only then consider preloading — and only for that one resource.
When LCP does not matter much
- Your LCP is already under 2.5 s at the 75th percentile. Further work has no measured benefit; go and look at the metric that is failing.
- Your audience is small and your traffic is mostly returning. LCP is still measured, but a fast cached experience is a different problem than a cold first load.
- The site is a text page with no images above the fold. You have essentially won by construction — system fonts, no hero image, and the sub-parts collapse to TTFB plus a little render time.
The practical workflow is short: measure, find the element, read the four sub-parts, and fix the dominant phase. Skipping the identification step is how people spend a week preloading the wrong file.
Written by TestedHost. Every recommendation on this site comes from running the setup described, on a live deployment — not from a vendor spec sheet. Spotted something out of date? Tell us.