What you'll learn: By the end of this lesson you will be able to say which element counts as the Largest Contentful Paint element and why it can change, split a slow LCP into its four parts, pick the right fix for each part (discoverability,
fetchpriority,preload, lazy loading, image bytes, render-blocking work, first byte), and spot cases where the metric looks good for the wrong reason.
Think of a magazine rack. A reader decides in a moment whether to pick up a magazine, and what they judge is the biggest picture on the cover. If the cover is printed late, the quality of the articles inside does not help.
The Largest Contentful Paint (LCP) metric puts a number on that moment for a web page: when the biggest piece of content in view was painted. Loading Performance and Core Web Vitals introduced LCP next to INP and CLS. This lesson goes deeper on LCP alone: how the browser picks the element, how to find the slow part, and what each fix costs.
The running example is a category landing page of an online store: a large hero banner, a promotional heading and a grid of product cards. Marketing replaces the hero image every week.
Facts below were read on 2026-11-08 from web.dev (the LCP page, last updated 4 September 2025, and the optimization guide, last updated 31 March 2025) and the W3C Largest Contentful Paint Working Draft of 26 August 2026. At the time of writing web.dev rates LCP "good" at 2.5 seconds or less and "poor" above 4.0 seconds, at the 75th percentile of page loads, segmented across mobile and desktop. Drafts and browser behaviour change; check before you build alerts on details.
The core idea
LCP is the render time of one element: the largest content element that has been painted in the viewport so far, measured from the start of navigation. Two consequences drive everything in this lesson.
- You must know which element it is. Optimizing the hero image does nothing if the LCP element on a phone is a block of text.
- The time is a chain, not a single cost. The element has to be found, requested, downloaded and painted, and each link has a different cause and a different fix. Web.dev splits LCP into four parts: time to first byte (TTFB), resource load delay, resource load duration and element render delay.
First find the element, then the largest of the four parts, then fix that part. Anything else is guessing.
Which element is the LCP element
The browser watches paints and keeps the biggest eligible element it has seen.
Figure 1. How an element becomes (or stays) the LCP element
url background or text block?"} B -->|"no"| X["Not a candidate"] B -->|"yes"| C{"Excluded? Opacity 0,
full viewport, low content"} C -->|"yes"| X C -->|"no"| D["Size = visible area
(images: smaller of visible and intrinsic)"] D --> E{"Larger than the
current LCP element?"} E -->|"yes"| F["New LCP entry reported"] E -->|"no"| G["Current LCP element stays"]
How to read this: LCP is a running maximum. Every larger element that finishes painting replaces the earlier one, until the user interacts.
What the sources say about each step:
- Types.
<img>(first frame for animated images),<image>inside<svg>,<video>(poster or first frame), elements with a CSSurl()background (not gradients) and block-level elements containing text. - Exclusions. Chromium skips elements with opacity 0, elements covering the whole viewport (likely a background) and low-entropy placeholder images. The W3C draft states the same ideas as steps: an image whose response is smaller than the area times 0.004 bytes is skipped, as is an element whose size equals the viewport. It is a working draft, so treat those numbers as current text, not a contract.
- Size. Only the part inside the viewport counts. For a resized image, web.dev says the smaller of the visible and intrinsic sizes is used, so upscaling a small image does not make it a bigger candidate. For text, the smallest rectangle containing the text nodes; margins, padding and borders are ignored.
- Not yet rendered. An image that has not loaded, or text still in the font block period (invisible fallback), is not a candidate.
- Removal. Removing an element does not reset the metric: it stays the largest "unless a larger element is rendered".
- Stopping. Reporting stops at the first tap, scroll or key press (the specification: the algorithm terminates when scroll or input events occur).
Illustration 1: the minimal case
<h1>Spring boots</h1>
<img src="/img/hero-1200.jpg" width="1200" height="600" alt="A row of leather boots">
<p>Free returns on all orders.</p>
The heading is painted first, so it is the first LCP candidate. When the hero image is painted and its visible area is larger than the heading's text rectangle, a new entry replaces it, and the reported LCP is the image's paint time. If the image fails to load, the heading stays the LCP element and the metric looks fast because the page is broken. The element can also differ by screen: on a narrow phone a long heading can push the hero down and a text block may win, so check field data. The API ignores elements inside iframes, though the metric counts them.
The four parts, and how to read them
The four parts add up to the LCP value when the element depends on a resource such as an image. For text, the middle two parts do not apply.
Figure 2. The four parts on one timeline
How to read this: nothing in parts 2 to 4 can start before part 1 ends, and part 3 cannot start before part 2 ends. A late start is a different problem from a slow download.
Web.dev gives rough ideal proportions: about 40 percent TTFB, under 10 percent load delay, about 40 percent load duration and under 10 percent render delay, as guidelines, not absolute time targets. They show an LCP out of balance: a load delay of a third of the total is the first place to look, though web.dev adds that if LCP is consistently within 2.5 seconds the proportions matter little.
An illustrative breakdown (invented numbers) of a poor LCP on the landing page:
| Part | Time | Share of 3.6 s | Reading |
|---|---|---|---|
| TTFB | 0.9 s | 25% | Below the roughly 40% guide, so not the main problem |
| Resource load delay | 1.2 s | 33% | Far above the under-10-percent guide: the image is found late |
| Resource load duration | 1.0 s | 28% | Below the guide, so the file is not the main problem |
| Element render delay | 0.5 s | 14% | Slightly high: something holds back the paint |
The largest anomaly is the load delay, so that is where to start; shrinking the image first would help less.
Where to get the parts: the Chrome DevTools Performance panel (live metrics, and Insights with an LCP breakdown); PageSpeed Insights for CrUX field data and a Lighthouse run; and, for your own users, the web-vitals/attribution build, whose LCPAttribution object carries target, url, timeToFirstByte, resourceLoadDelay, resourceLoadDuration and elementRenderDelay (the README's examples use web-vitals@5 at the time of writing; the middle two are 0 for text). Send them to your own collector (see the Core Web Vitals lesson) and aggregate per page template.
Fixing the load delay: make the resource discoverable and urgent
The browser's preload scanner looks ahead through the HTML as it arrives and starts requests for what it finds, so an <img> in the HTML is found at once. A CSS background image is not discoverable from the HTML, nor is an image URL that exists only after JavaScript or an API call.
Figure 3. Discoverable in the HTML, or hidden behind script
How to read this: the second branch adds two network round trips (the bundle and the data) and a script run before the image request even starts. All of that time is load delay.
The fixes, in the order to try them:
- Put the image in the HTML as an
<img>(server-rendered or static). This is an architecture decision: see Rendering Strategies. - Raise the priority with
fetchpriority="high"on the likely LCP image. Web.dev's page on Fetch Priority says images start at low priority and are boosted to high at layout time if they are within the viewport, and that as of Chrome 117 the first five large images are set to medium. The attribute lets the important image start at high at once. It is a hint, not a directive; use it on one or two images, because if everything is high, nothing is. That page lists support from Chrome 102, Edge 102, Firefox 132 and Safari 17.2 at the time of writing. - Never lazy-load the LCP image. Web.dev says lazy-loading the LCP image "will always lead to unnecessary resource load delay". In web.dev's lazy-loading experiment (a demo WordPress site, WebPageTest, medians of nine runs) the desktop archive page had an LCP median of 2,029 ms with default lazy loading and 1,759 ms with it disabled, while the single-post page differed little (1,655 ms against 1,726 ms). One lab experiment: the direction, not a size to expect.
- Use
rel="preload"when the image cannot be in the HTML, for example a CSS background image or an image chosen by script. Web.dev's preload article says preload, unlike preconnect, "is mandatory for the browser", and an unused one triggers a Chrome console warning about 3 seconds afterload. For responsive images useimagesrcsetandimagesizeson the<link>, which take the same syntax assrcsetandsizes:
<link rel="preload" as="image" href="hero-800.avif"
imagesrcset="hero-400.avif 400w, hero-800.avif 800w, hero-1600.avif 1600w"
imagesizes="100vw" fetchpriority="high">
Web.dev's caveats (page updated 10 July 2026): responsive preloads may not work in a Link HTTP header, cannot be used in 103 Early Hints, have no equivalent for <picture>, and should be avoided for an image that is quickly discoverable in the HTML.
5. Send 103 Early Hints when the server needs time to build the page. Chrome's documentation (updated 10 July 2026) describes a preliminary response for navigation requests that supports preconnect and preload (not prefetch); only cacheable resources can be preloaded this way, or they are fetched twice. Listed support: Chrome and Edge 103, Firefox 120 (preconnect) and 123 (preload), Safari 17 (preconnect only). When the server can answer at once, a tag in the HTML is enough.
Fixing the load duration: fewer, closer bytes
The guide notes this part is often not the main bottleneck, so check field data first. Its list: an image sized for its slot, AVIF or WebP with compression, the same origin as the HTML (preconnect reduces the cost of another origin, but same-origin is better), a CDN, a long cache policy for repeat visits, and fetchpriority="low" for images out of view, such as hidden carousel slides. See HTTP Caching for the Front End and CDN and Object Storage.
Fixing the render delay: the image arrived but is not painted
Render delay is the gap between the resource finishing and the element appearing. Web.dev's guide names a stylesheet that takes longer to load than the LCP image, synchronous scripts in the <head> ("almost never necessary"), an element hidden until JavaScript reveals it (its example is an A/B testing library), an element not yet in the DOM because it waits for JavaScript (the remedy is server-side rendering), and long tasks, because images render on the main thread. Remedies: inline CSS only if it is small, avoid synchronous head scripts, render the element on the server, break up long tasks (see The Event Loop and the Main Thread and The Critical Rendering Path). For a text LCP element, fonts matter: text in the font block period is invisible and cannot be a candidate, so use a font-display value other than auto or block.
Fixing the first byte
The guide's advice: minimise redirects, avoid unique URL parameters (analytics ones, say) that stop a CDN serving cached content, and use a CDN. Server-side rendering adds processing time that can slow TTFB, which the guide still considers worthwhile; build-time HTML is generally better where the architecture allows it.
Choosing fixes: trade-offs
| Fix | Part it moves | Cost or risk |
|---|---|---|
Hero <img> in server-rendered HTML |
Load delay, render delay | Rendering infrastructure; hydration cost |
fetchpriority="high" |
Load delay, duration (less contention) | A hint; overuse removes its value |
rel="preload" |
Load delay | Mandatory fetch: a wasted download if the markup changes; responsive and <picture> caveats |
103 Early Hints |
Load delay, TTFB overlap | Server and CDN support; a non-cacheable resource is fetched twice; no responsive preloads |
| Smaller formats and sizes | Load duration | Build step; quality review |
| Same-origin or preconnected hosting | Load duration | Hosting and cache changes |
| Defer or inline CSS and scripts | Render delay | Inlining too much grows every HTML response |
| HTML at the edge | TTFB | Invalidation and stale content |
Illustration 2: the landing page, diagnosed
The landing page's hero is injected by a slider library: the HTML holds an empty container, and a script reads banner JSON and sets a CSS background-image. The 3.6 s breakdown says "load delay", and Figure 3's second branch shows why. The fix is an <img> in the HTML, not a smaller file.
Illustration 3: when LCP is fast for the wrong reason
A site shows a full-width splash image for the first second, then removes it. Because removed content is still considered, the early-painted splash can become the LCP element and stay it unless a larger element renders. The reported LCP is the splash's time, which looks excellent while the real content arrives much later. The W3C draft lists this under known limitations (removed content is kept for image carousels, at the cost of problems for splash screens with large placeholders); pages loaded off-screen, such as in a background tab, report larger values than expected. When a number looks too good, collect target and url with the time.
In practice: the weekly hero banner
This is a realistic scenario, not a case history. It is a composite built from common patterns, and every number in it is an illustrative assumption.
The situation
The store's category landing page uses a headless CMS, and marketing swaps the hero banner each week. Field LCP at p75 on mobile is 3.6 s (illustrative), poor under the 2.5 s target. RUM with attribution shows the largest part is resource load delay.
Starting architecture
Figure 4. Starting architecture: the hero is created by script
How to read this: the numbers are the order of requests. The image, the thing that is the LCP element, is requested last.
Options considered
| Option | Helps | Costs and risk |
|---|---|---|
| Compress the hero harder | Load duration | Cheap, but not the largest part |
| Add a preload for the current banner | Load delay | The URL changes weekly: the preload must be generated with the page, or it fetches a stale image |
Render the banner in the HTML on the server, with fetchpriority="high" |
Load delay, render delay | Server work; the CMS API must be reachable at render time; cache invalidation on the weekly swap |
The decision
| Field | Decision record |
|---|---|
| Context | Field data shows the load delay is the largest part of LCP on the landing template. |
| Decision | Server-render the hero as an <img> with width, height, srcset, sizes and fetchpriority="high"; never lazy-load it. Serve it through an image pipeline with AVIF or WebP and a fallback. Cache the HTML for a short time and invalidate on publish. |
| Consequences, good | The image request starts as soon as the first HTML bytes are parsed. |
| Consequences, bad | More rendering work at the origin and a cache to purge; the slider's behaviour must work without the script's first paint. |
| Revisit when | Field p75 LCP is not under 2.5 s two weeks after release, or the largest part moves to TTFB. |
Figure 5. Target architecture: the hero is in the HTML and served from the edge
How to read this: the renderer reads the banner from the CMS while building the HTML, so the browser never has to ask for it. The image request leaves right after the first parse.
Rollout and verification
- Ship attribution reporting first, tagged by template and release, and record a baseline for each part.
- Release to a share of traffic and compare the parts, not only the total: load delay should fall while TTFB does not grow enough to cancel the gain.
- Add a CI check that fails if the hero
<img>hasloading="lazy"or lacks dimensions.
The 2 a.m. failure
At 02:00 a scheduled weekly banner goes live. The editor uploaded a large unprocessed file that bypassed the image pipeline. The RUM alert on LCP p75 for the landing template fires, and the attribution shows load duration, not load delay, as the largest part. First checks: the image URL in the attribution data, its size in the CDN logs and whether the pipeline ran. Mitigation: replace the banner with the processed version, then purge the cached HTML. Follow-up: make the CMS refuse oversized uploads and generate the responsive set on upload.
Architect's notes
- Decide per template which element should be the LCP element, who owns it, and where the browser discovers it. Write it in the page spec.
- Treat the hero's markup as a contract that CI can check, and prefer discoverability over workarounds: preload and Early Hints repair a late discovery; an
<img>in the HTML removes it. - Measure parts, not just the total, keep the LCP element in the field data, and distrust good numbers from splash screens, background tabs and broken images.
Common mistakes
- Optimizing the wrong part, such as compressing images when the load delay is largest.
- Lazy-loading the hero, marking every image
fetchpriority="high", or hiding the hero behind client-side rendering, an entrance animation or a render-blocking script. - Preloading a URL that changes without generating the preload from the same data as the markup.
- Assuming the LCP element is the same on every device, or never recording which element it was.
Key takeaways
- LCP is a running maximum over eligible, painted elements until the first interaction; removed elements still count.
- Find the element, then the largest of the four parts, then fix that part.
- Load delay is the cheapest win: an
<img>in the HTML beats any workaround. fetchpriorityis a hint for one or two images; preload is a mandatory fetch and needs care. Never lazy-load the LCP image.- Render delay comes from blocking CSS and scripts, hidden elements and long tasks.
- Check that a good LCP describes real content, and keep the element and its parts in field data so alerts tell you what to fix.
Practice exercises
Exercise 1 — Starter (30 min): In Chrome DevTools find a page's LCP element at desktop and phone window sizes. Are they the same? Which of the four parts looks largest?
Exercise 2 — Intermediate (2 hours): Build a page whose hero is a CSS background-image. Record the four parts, then move it into the HTML as an <img> with fetchpriority="high" and compare.
Exercise 3 — Intermediate (2 hours): Log target, url and the part durations with web-vitals/attribution. Add loading="lazy" to the hero on purpose and see which part grows.
Exercise 4 — Advanced (half day): Write the CI check from the rollout: fail a build when the hero <img> lacks width and height or uses loading="lazy", or when more than two images carry fetchpriority="high". What can the check not see?
Related reading
In this course:
- Loading Performance and Core Web Vitals — the three metrics and the monitoring pipeline.
- Cumulative Layout Shift and Layout Stability — what breaks when a hero loads late.
- The Critical Rendering Path — what blocks first paint.
- The Event Loop and the Main Thread — long tasks and render delay.
- HTTP Caching for the Front End — caching images and HTML.
- Rendering Strategies: CSR, SSR, SSG, ISR and Streaming — where the HTML is produced.
Elsewhere on the site:
- System Design: CDN and Object Storage — edge delivery of images and HTML.
- System Design: Caching Basics — the server side of a fast first byte.
Official documentation
- web.dev: Largest Contentful Paint (LCP) — definition, elements, thresholds (read 2026-11-08).
- web.dev: Optimize Largest Contentful Paint — the four parts and fixes (read 2026-11-08).
- W3C: Largest Contentful Paint — Working Draft of 26 August 2026.
- web.dev: Optimize resource loading with Fetch Priority —
fetchpriorityand default image priorities. - web.dev: Preload responsive images —
imagesrcset,imagesizesand caveats. - Chrome for Developers: Early Hints —
103 Early Hints, limits and support.