skip to content

A page's Largest Contentful Paint (LCP) comes in at 4.2 seconds. How do you break that single number into its component phases to find where the time actually goes?

level: middleimportance: must knowfreq 65%

answer

  1. one timestamp, four consecutive phases
  2. two phases are cost, two are waste
  3. roughly 40 / 10 / 40 / 10
  4. text LCP has no resource phases
  5. LCP entry plus Resource Timing entry

basics

~20 s

Split LCP into four consecutive phases: time to first byte, resource load delay (before the LCP resource starts downloading), resource load duration, and element render delay. Each phase points at a different cause, and a text LCP has both resource phases at zero.

solid answer

~60 s

LCP is a timestamp, so it decomposes into four back-to-back phases that always sum to the whole number. First, **time to first byte** — everything up to the first byte of the document. Second, **resource load delay**: the gap between that and the moment the browser actually starts fetching the LCP element's resource; this is discovery and prioritisation time. Third, **resource load duration**: how long that fetch takes. Fourth, **element render delay**: from the resource being available to the element actually painting. A useful rule of thumb for a 2.5s budget is roughly 40% TTFB, 10% load delay, 40% load duration, 10% render delay — the two delay phases should be small, and when they are not, that is your answer. If the LCP element is text there is no resource, so both middle phases are zero and the time lives entirely in TTFB and render delay. I get the numbers from the LCP entry plus the matching Resource Timing entry, or from a RUM library that reports the attribution for me.

code

javascript · 12 lines
javascript
new PerformanceObserver((list) => {
  const lcp = list.getEntries().at(-1);
  const nav = performance.getEntriesByType('navigation')[0];
  const res = lcp.url ? performance.getEntriesByName(lcp.url)[0] : undefined;

  const ttfb = nav.responseStart;
  const loadDelay = res ? res.startTime - ttfb : 0;
  const loadDuration = res ? res.responseEnd - res.startTime : 0;
  const renderDelay = lcp.startTime - (res ? res.responseEnd : ttfb);

  console.table({ ttfb, loadDelay, loadDuration, renderDelay, lcp: lcp.startTime });
}).observe({ type: 'largest-contentful-paint', buffered: true });

go deeper

for a junior

Learn the four phase names in order — TTFB, resource load delay, resource load duration, element render delay — and that together they add up to the LCP number.

for a middle

Explain what each phase measures and which cause it points to, and say which two phases are pure waste rather than necessary work. Know why both middle phases are zero for a text LCP.

for a senior

Demonstrate that you read the phases before choosing a fix, and be ready to explain the cross-origin Timing-Allow-Origin gap that hides the split for exactly the third-party image you need to inspect.

for a principal

Turn the breakdown into an allocation decision: which phase your organisation actually controls, which team owns it, and how you stop every LCP regression from being routed reflexively to the frontend team.

## Why a single number is not actionable "LCP is 4.2s" tells you the page felt slow and nothing else. Two pages with identical LCPs can need opposite fixes — one needs a faster server, the other needs the hero image referenced in the HTML instead of injected by script. The standard move is therefore to decompose the timestamp into consecutive, non-overlapping phases. Because they are consecutive and cover the whole interval, they must add up to LCP, which makes the breakdown easy to sanity-check. ## The four phases **1. Time to first byte (TTFB).** From the start of the navigation to the first byte of the HTML response. This includes DNS, connection setup, any redirects, and server think time. Nothing else can start until it ends, so every later phase is pushed back by exactly this much. **2. Resource load delay.** From TTFB to the moment the browser *begins* fetching the LCP element's resource. This phase measures nothing but discovery and queuing: how long it took the browser to learn the resource exists and decide to fetch it now. A big number here means the resource was invisible to the preload scanner, was deprioritised, was queued behind other downloads, or simply did not exist in the markup yet. **3. Resource load duration.** From the start of that fetch to its end. This is bytes over the wire: file size, format, compression, connection quality, and contention with other in-flight requests. **4. Element render delay.** From the resource being available to the element actually painting. Ideally near zero. When it is large, the pixels were ready and something else stopped them appearing: render-blocking CSS or scripts still outstanding, a long main-thread task hogging the CPU, markup that only exists after client-side rendering, or text waiting on a webfont. ## The rough budget A widely used rule of thumb allocates a 2.5-second LCP budget as approximately: | Phase | Share | Rough figure | | --- | --- | --- | | TTFB | ~40% | ~800 ms | | Resource load delay | <10% | ~200 ms | | Resource load duration | ~40% | ~1000 ms | | Element render delay | <10% | ~200 ms | The shape matters more than the exact figures. Two of the phases are *work* — the server producing bytes, the network moving bytes — and two are *delays* that produce nothing at all. If either delay phase is materially over its share, you have found waste rather than cost, and waste is almost always the cheaper thing to remove. ## The text-LCP special case If the winning candidate is a block of text, there is no resource to fetch for it, so load delay and load duration are both zero by definition and LCP reduces to TTFB plus render delay. Candidates who have only ever debugged hero images get stuck here, hunting for an image request that does not exist. For text LCP the questions are: how fast is the document, is CSS or JavaScript blocking the first render, is the text waiting for a font, and does the text exist in the HTML at all or only after hydration. ## Getting the numbers In the browser you can derive all four from two APIs: the `largest-contentful-paint` entry gives you the final timestamp and the candidate's `url`, and `performance.getEntriesByName(url)` gives you the Resource Timing entry for that fetch, whose `startTime` and `responseEnd` bracket phases 2 and 3. `performance.getEntriesByType('navigation')[0].responseStart` gives TTFB. ```js const nav = performance.getEntriesByType('navigation')[0]; const res = performance.getEntriesByName(lcpEntry.url)[0]; const ttfb = nav.responseStart; const loadDelay = res ? res.startTime - ttfb : 0; ``` One caveat that bites in production: if the LCP image is served cross-origin without a `Timing-Allow-Origin` response header, the Resource Timing entry's detailed timings are zeroed, and you cannot split load delay from load duration for exactly the resource you most want to inspect. Adding that header to your image host is a prerequisite for real-user LCP attribution, not an optional extra. ## How to use the split Read the phases in order and stop at the first one that is out of proportion. A dominant TTFB is a backend or delivery problem and nothing you do in the page will fix it. A dominant load delay is a discovery problem — the browser learned about the resource too late. A dominant load duration is a bytes problem. A dominant render delay is a main-thread or blocking-resource problem. This ordering also prevents the classic failure mode: compressing the hero image, shipping it, and finding LCP unchanged because load duration was only 12% of the total to begin with.

  • How would you get this phase split for real users rather than for one lab run?
    Report it from the page itself: pair the final LCP entry with the Resource Timing entry for its URL and send the four numbers with your other field data, or use a RUM library whose attribution build already computes them. The lab run tells you about one device on one network; the field distribution tells you whose loads are actually breaking.
  • The LCP image is served from a third-party image host and its load delay and load duration both come back as zero. What is wrong?
    Cross-origin resources expose only coarse timing unless the response carries a `Timing-Allow-Origin` header permitting your origin. Without it the Resource Timing entry's detailed marks are zeroed, so the phases cannot be split. Fix the header on the image host; until then you can only see total LCP, not where it went.
  • If TTFB alone is 1.9 seconds, is there any point optimising the rest of the page?
    There is, but the ceiling is set. Every later phase starts after TTFB, so a 1.9s TTFB makes a 2.5s LCP nearly unreachable no matter how small the image is. Fix the document response first — origin latency, redirects, caching at the edge — then re-measure before touching the page.

saying these in an interview costs you the question

  • Jumps straight to compressing the hero image without checking the phases
  • Assumes a slow LCP always means a slow server
  • Thinks a text LCP still has a resource download phase
  • Treats element render delay and TTFB as interchangeable
  • Believes the phases can overlap or double-count

context