A page has a poor Largest Contentful Paint. How do you diagnose it and what do you actually change?
Split the LCP into TTFB, resource load delay, resource load time and render delay, use field attribution rather than a single lab run to see which part dominates, then fix that part specifically - prioritise the hero image, stop lazy-loading it, and remove render-blocking work.
What the interviewer is scoring
- Whether you decompose LCP into its four sub-parts before proposing any fix
- Whether you ask what the LCP element actually is instead of assuming it is an image
- Whether you can explain why a Lighthouse score and CrUX field data legitimately disagree
- Whether you know that preload only helps when the resource is undiscoverable by the preload scanner
- Whether you notice that deferring JavaScript to help LCP can push the cost onto INP
Answer
First, establish what the metric is measuring
LCP is the render time of the largest content element visible in the viewport, measured from navigation start. The candidate can be an <img>, an image inside an <svg>, a video's poster frame, an element whose background is a CSS url() image, or a block-level element containing text. It is a moving target during load: the browser reports a new candidate every time a larger one paints, and the final value is the last candidate reported before the user first interacts or the page is hidden. Good is 2.5 seconds or less at the 75th percentile of page loads, poor is above 4 seconds, and the band between the two is "needs improvement".
The first diagnostic question is therefore not "how do I make images faster" but "which element is the LCP element, and is it the element I want it to be?" A page whose LCP candidate turns out to be a cookie banner or a paragraph of boilerplate has a different problem from one whose LCP is a legitimate hero image.
Split it into four sub-parts
Every LCP decomposes into four consecutive spans, and they are mutually exclusive, so they sum to the whole metric.
- Time to first byte — navigation start until the first byte of the document arrives. Server think time, redirects, DNS and TLS, and cache misses all land here.
- Resource load delay — first byte until the browser starts fetching the LCP resource. This is pure discovery latency: the image existed all along, but the browser did not know to ask for it yet. Zero for a text LCP.
- Resource load time — how long the fetch itself takes. Bytes over the wire.
- Element render delay — the resource finished downloading, or the text was ready, but nothing painted yet. Render-blocking stylesheets, synchronous scripts, blocked font loads and long main-thread tasks all sit here.
web.dev suggests treating roughly 40% TTFB, 10% load delay, 40% load time and 10% render delay as the target shape. The exact split matters less than the discipline: find the dominant span first, because a fix aimed at the wrong span produces no measurable change.
Get the breakdown from the field, not just from lab
Lab and field data disagree constantly, and usually for legitimate structural reasons rather than because one is wrong. Lighthouse runs one cold load, with an empty cache, on one device profile, under simulated network throttling, and never scrolls or interacts. CrUX field data is the 75th percentile over a rolling window of real loads, mixing warm and cold caches, real devices from cheap Android upward, real networks, and users who scroll and click. So a page can score well in Lighthouse and fail in the field because the p75 device is far slower than the lab profile, or score badly in Lighthouse and pass in the field because most real visits arrive with the hero image already cached.
There is also a metric that only exists in the field. INP replaced FID as a Core Web Vital in March 2024, and it requires real interactions, so Lighthouse cannot report it at all — it reports Total Blocking Time as a lab proxy. Anyone who says "Lighthouse showed our INP is fine" has told you they have not looked.
So collect the sub-part attribution from real users with the web-vitals attribution build, which reports the LCP element, its URL and each of the four spans.
import { onLCP } from 'web-vitals/attribution';
onLCP(({ value, attribution }) => {
// element and url identify WHAT was slow; the four spans identify WHICH PART.
send({
value,
element: attribution.target,
url: attribution.url,
ttfb: attribution.timeToFirstByte,
loadDelay: attribution.resourceLoadDelay,
loadTime: attribution.resourceLoadDuration,
renderDelay: attribution.elementRenderDelay,
});
});
Fixing each span
Load delay is where the cheap wins are, because it is usually self-inflicted. The hero must be discoverable by the preload scanner and must be prioritised above everything else fighting for the same connection. Give it fetchpriority="high", since browsers assign low priority to images by default until layout proves they are in the viewport. Never put loading="lazy" on the LCP element: that defers the fetch until layout has run, which guarantees a load delay that no amount of CDN tuning recovers. If the image is a CSS background or is injected by JavaScript, the preload scanner cannot see it in the markup at all, and that is the case where <link rel="preload"> genuinely earns its place — though converting it to a real <img> is usually the better fix.
<!-- The hero: high priority, eagerly fetched, and dimensioned so it cannot shift layout. -->
<img src="/hero-800.webp"
srcset="/hero-800.webp 800w, /hero-1600.webp 1600w"
sizes="(max-width: 800px) 100vw, 800px"
fetchpriority="high" width="1600" height="900" alt="...">
<!-- Only needed when the scanner cannot find it in markup, e.g. a CSS background-image. -->
<link rel="preload" as="image" href="/hero-800.webp"
imagesrcset="/hero-800.webp 800w, /hero-1600.webp 1600w"
imagesizes="(max-width: 800px) 100vw, 800px" fetchpriority="high">
Load time is a bytes problem: serve modern formats, size the image to the actual layout box rather than shipping a 2400px asset into a 600px slot, and serve it from a CDN. TTFB is a server and edge problem: cache the document, avoid redirect chains, and preconnect to the host serving the hero if it differs from the document origin.
Render delay is where framework choices show up. A synchronous <script> in the head blocks parsing; a stylesheet blocks rendering until it is fetched and parsed, so route-specific CSS should not be in the global blocking bundle. A text LCP in a client-rendered app cannot paint until the bundle downloads, parses, executes and fetches data, which is why the same page server-rendered often has a dramatically better LCP with identical assets.
How CLS and INP ride along
The same work moves the other two vitals, sometimes in the wrong direction. CLS is good at 0.1 or below and poor above 0.25, and image work touches it directly: an image that arrives late without width/height or aspect-ratio reserves no space and shifts everything below it when it lands. Fonts cut both ways — font-display: swap lets text paint early, which helps LCP, but the swap to the real face reshapes the text and can register a shift, whereas font-display: optional protects CLS at the cost of sometimes never using your font.
INP is good at 200ms or below and poor above 500ms, and it is where LCP fixes most often backfire. Deferring a large bundle so it stops blocking the first paint does not delete the work; it relocates it. The bundle now executes slightly later, still on the main thread, quite possibly at the exact moment the user taps the navigation. Because INP is measured across the whole visit rather than during load, you can improve LCP and regress INP with one change. Reducing the total amount of JavaScript, splitting long tasks, and yielding to the main thread between chunks of work help both metrics; merely reordering the script tags helps one and hides the other.
The trap
The trap is reaching for <link rel="preload"> as the reflexive answer. Preload only removes discovery latency, so it does nothing for an <img> that is already sitting in the served HTML where the preload scanner found it before the parser even reached it. Worse, preloads are a zero-sum instruction: every additional high-priority preload competes with the ones that matter and with the render-blocking CSS, so a page that preloads six things has effectively prioritised nothing. A strong answer asks whether the resource is discoverable, and only then decides between fetchpriority, a preload, or removing whatever is starving the connection.
LCP is not one number to optimise, it is four spans in sequence — measure which span dominates in the field before you change anything, because a fix aimed at the wrong span is indistinguishable from no fix at all.
Likely follow-ups
- Your LCP element is a text block rendered client-side after a data fetch. Which sub-part is the problem and what do you change?
- Why can adding several preloads make LCP worse rather than better?
- How would you measure LCP for a single-page app where the interesting content appears after a client-side route change?
- The p75 LCP is 4.1 seconds but the median is 1.6 seconds. What does that shape tell you and where do you look next?
Related questions
- Field data says your page has an INP of 400 ms. How do you find the cause and fix it?hardAlso on core-web-vitals4 min
- Design the frontend for a data-heavy operational dashboard with live updates.hardAlso on performance-budget7 min
- A dashboard has been showing the same temperature for two days and nobody noticed. Walk the path and tell me what would have caught it.hardSame kind of round: scenario6 min
- Everyone in the business calls it the fax queue, and nothing has been faxed since 2014. Do you keep that name in the code?mediumSame kind of round: concept4 min
- How does your order placement change on a venue that allocates pro rata within a price level instead of first in, first out?hardSame kind of round: concept6 min
- Your A/B test came back not significant. What do you do next?hardSame kind of round: scenario4 min
- Leadership wants DORA metrics on a dashboard for every team. How do you use them well, and how would each one be gamed?hardSame kind of round: concept4 min
- A customer asks how you meet a control your cloud provider handles. What can you claim from their certification?mediumSame kind of round: concept5 min