Slow LCP: Fixing Largest Contentful Paint
Largest Contentful Paint records when the biggest image or text block in the visible part of the page finishes drawing. It is the Core Web Vital most sites fail, and visitors feel it as a page that stays blank or half-built while they wait for the thing they came for.
To pass, 75% of real visits must reach LCP in 2.5 seconds or less. This guide shows you how to find the exact element being measured, work out which of four stages is eating the time, and apply the fix that matches that stage rather than guessing.
What LCP actually measures
While a page loads, the browser keeps track of the largest piece of content it has painted inside the viewport so far. Each time something bigger appears, it becomes the new candidate. When the visitor first interacts with the page, by tapping, clicking or pressing a key, the browser stops looking, and the last candidate becomes the Largest Contentful Paint. Scrolling does not count as an interaction for this purpose on every browser, so treat the first screenful as what matters.
Only certain kinds of element can be the LCP element:
- Images in an
<img>tag, including the one chosen from a<picture>element or asrcset. - Images inside SVG drawn with the
<image>element. - Video poster images, and in current Chrome the first painted frame of a video.
- CSS background images loaded with
url(), as opposed to gradients. - Block-level elements containing text, such as a heading or a paragraph.
The size used is what is visible on screen, so an image that is partly off the edge only counts its visible area, and an image stretched beyond its natural size is counted at its natural size. Chrome also ignores elements with no visible content and very low-detail placeholder images, so a blurry placeholder usually does not steal the LCP from the real hero image that replaces it.
| LCP at the 75th percentile | Rating |
|---|---|
| 2.5 s or less | Good |
| Over 2.5 s up to 4 s | Needs improvement |
| Over 4 s | Poor |
The time is measured from when the visitor started navigating to your page, not from when the HTML arrived. Redirects, DNS lookups, connection set-up and a slow server all count towards it before a single byte of your hero image has been requested.
The four parts of LCP time
Every LCP time can be split into four consecutive stages. Knowing which stage is long tells you which fix to apply, and it stops you spending a day compressing an image when the real problem is that the browser did not ask for it until two seconds in.
| Stage | Runs from | Typical cause when it is long |
|---|---|---|
| Time to First Byte | Navigation start to the first byte of HTML | Slow hosting, no page cache, redirects, a distant server |
| Resource load delay | First byte to the moment the LCP resource is requested | Image found late: set by JavaScript, hidden in CSS, lazy-loaded, or queued behind other files |
| Resource load duration | Request start to the last byte of the resource | Oversized file, wrong format, slow or distant image host |
| Element render delay | Resource loaded to the element being painted | Render-blocking CSS or scripts, web fonts, client-side rendering, hidden-until-ready tricks |
For a text LCP element, such as a large heading, there is no resource to fetch, so the two middle stages are zero and the time is split between TTFB and render delay. That usually points at blocking stylesheets or a web font.
As a rough guide, a healthy page spends most of its LCP time in TTFB and in downloading the image, with the load delay and render delay both kept small. If either of the "delay" stages is the largest slice, you have a prioritisation problem rather than a file-size problem, and that is good news, because prioritisation fixes are usually a single line of HTML.
How to find your LCP element and its bottleneck
Start with the Core Web Vitals checker or PageSpeed Insights for the failing URL. Check the field result first, on mobile, to confirm the problem is real, then look at the lab diagnostics to see which element Lighthouse chose and how its time breaks down.
The lab element is not always the one real visitors see. A cookie banner, a different hero on small screens or personalised content can all change it. To see what your own browser records, open DevTools, go to the Console and paste this before reloading:
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
console.log('LCP candidate', Math.round(last.startTime), 'ms', last.element, last.url);
}).observe({ type: 'largest-contentful-paint', buffered: true });
Every new candidate is logged, so the final line is the LCP element. Repeat on a phone-sized viewport with the network throttled, because mobile layouts often have a different hero.
Collecting it from real visitors
For the full picture, record LCP from real visits using Google's open-source web-vitals
library with its attribution build. It reports the element selector and the time spent in each of the
four stages, which you can send to your analytics tool:
import { onLCP } from 'web-vitals/attribution';
onLCP(({ value, attribution }) => {
navigator.sendBeacon('/rum', JSON.stringify({
lcp: Math.round(value),
element: attribution.target,
ttfb: Math.round(attribution.timeToFirstByte),
loadDelay: Math.round(attribution.resourceLoadDelay),
loadDuration: Math.round(attribution.resourceLoadDuration),
renderDelay: Math.round(attribution.elementRenderDelay)
}));
});
Group the results by page template. If the same element and the same slow stage keep coming up, you have found the fix to make.
Common causes, stage by stage
Long TTFB
If the first byte takes more than about 800 ms for most visitors, fix that before anything else. Common causes are pages built from scratch on every request, uncached database queries, a server on another continent from your visitors, and chains of redirects such as http to https to www. The TTFB guide covers server-side fixes in detail.
Long resource load delay
- Lazy-loaded hero image. A
loading="lazy"attribute on the main image tells the browser to wait until layout is known, which delays the request. Many themes and plugins apply lazy loading to every image by default. - Image set by JavaScript. Sliders, carousels and lazy-load libraries that keep the
real address in
data-srchide the image from the browser's preload scanner until the script runs. - CSS background image. The browser only learns about a background image after it has downloaded and parsed the stylesheet and matched the rule to an element.
- Client-side rendering. If the HTML is an empty shell and the page is built by a JavaScript framework, the image cannot be requested until the bundle has downloaded and run.
- Normal priority. Images start at low priority in Chrome until layout shows they are in the viewport, so the hero can queue behind scripts and fonts.
Long resource load duration
The file is simply too big for the connection: a 2,400-pixel-wide JPEG sent to a 400-pixel phone screen,
no modern format, or an image served from a separate domain that needs its own connection set up. Serve
AVIF or WebP, use srcset and sizes so small screens get small files, and host
images on the same origin or a CDN that is already connected.
Long element render delay
The image has arrived but the browser is not allowed to paint it yet. Blocking stylesheets and synchronous
scripts in the <head> are the usual reason. Others are A/B testing snippets that hide
the page until an experiment loads, fade-in animations that start at zero opacity, a hero that is only
revealed once a slider script initialises, and text headings waiting for a web font with a long block
period.
The key HTML changes
For most image heroes, the fix is a correctly written image tag that the browser can find in the first chunk of HTML and knows is important:
<img
src="/images/hero-1200.avif"
srcset="/images/hero-600.avif 600w, /images/hero-1200.avif 1200w, /images/hero-1800.avif 1800w"
sizes="100vw"
width="1200" height="675"
alt="Walkers on the coastal path above Hartcliffe Bay"
fetchpriority="high"
decoding="async">
There is no loading attribute, which means eager loading. fetchpriority="high"
tells the browser to request it ahead of other images, and it is now supported across Chrome, Edge, Safari
and Firefox. The width and height reserve space so the image does not also cause layout shift.
When the image cannot be in the HTML
If the hero must stay a CSS background, or it is inserted by a framework, preload it so the request starts straight away:
<link rel="preload" as="image"
href="/images/hero-1200.avif"
imagesrcset="/images/hero-600.avif 600w, /images/hero-1200.avif 1200w"
imagesizes="100vw"
fetchpriority="high">
Put it high in the <head>, before stylesheets and scripts. Preload only the one
LCP image per template; preloading several images makes them compete with each other and with your CSS.
Images on another domain
<link rel="preconnect" href="https://images.example-cdn.com" crossorigin>
A preconnect lets the browser do the DNS lookup, connection and TLS handshake for an image host early. Use it for the one or two origins that serve above-the-fold content, not for every third party.
fetchpriority="high" to every image "just in case". If
everything is high priority, nothing is, and you can slow down the stylesheet the page needs to render.Platform notes
- WordPress. Since version 6.3, WordPress core adds
fetchpriority="high"to the image it judges most likely to be the LCP element and skips lazy loading for it, but page builders, sliders and optimisation plugins can override that. Check the rendered HTML, not the setting screen. See Core Web Vitals in WordPress. - Shopify. The Liquid
image_tagfilter acceptsloadingandfetchpriorityparameters, so the hero section can be written to load eagerly with high priority. See Core Web Vitals in Shopify. - Next.js. Mark the hero
next/imageas the priority image (thepriorityprop, orpreloadin newer versions) so it is preloaded and not lazy-loaded. See Core Web Vitals in Next.js and Nuxt. - Static and custom sites. You control the markup, so apply the tag above directly and make sure your build tool is not rewriting it with a lazy-load attribute.
The fix, step by step
Step 1
Confirm the failure in field data
Run the failing URL through the Core Web Vitals checker and note the mobile field LCP at the 75th percentile, then open the Core Web Vitals report in Search Console to see which group of similar URLs it sits in. Field data tells you whether real visitors have a problem; if there is no field data for the page, use the origin figure and treat lab results as a guide only. Write the baseline down so you can compare later.
Step 2
Identify the LCP element
Read
the element named in the lab diagnostics, then confirm it yourself by running a PerformanceObserver
for largest-contentful-paint in the DevTools console on a phone-sized viewport with throttling
on. Note whether it is an image tag, a CSS background, a video poster or a text block, and whether it
is the same on desktop. Check for a cookie banner or popup becoming the element instead of your intended
hero.
Step 3
Break the time into its four parts
Use the LCP breakdown in PageSpeed Insights or the DevTools Performance panel to split the time into Time to First Byte, resource load delay, resource load duration and element render delay. Find the largest slice. A long first stage sends you to server work, long delays mean a discovery or blocking problem, and a long download means the file itself is too heavy for the connection your visitors use.
Step 4
Make the element discoverable in the HTML
View the page source rather than the inspected DOM and search for the hero image address.
If it only appears in a stylesheet, a data-src attribute or a JavaScript bundle, change
the template so a normal <img> with a real src and srcset
is in the server-sent HTML. Where that is impossible, add a single rel="preload" link for
the image near the top of the head.
Step 5
Give it top priority and stop lazy-loading it
Remove loading="lazy" from the hero image and from anything else in the first
screenful, and add fetchpriority="high" to the LCP image only. If the image lives on a different
host, add a preconnect for that origin. Check that theme settings, lazy-load plugins and
image components are not adding the lazy attribute back when the page is rendered, then reload and confirm
the request starts early.
Step 6
Shrink the file and shorten the render delay
Serve the hero as AVIF or WebP at sizes that match real screens, using srcset
and an accurate sizes value so phones do not download desktop images. Then clear the path
to painting it: inline the small amount of CSS the first screen needs, defer non-essential scripts, remove
fade-in effects that start at zero opacity, and give headline fonts font-display: swap or
a close fallback.
Step 7
Verify in the lab, then in the field
Rerun the lab test several times on mobile settings and compare the four stages with your baseline, not just the headline number. Deploy to every page that shares the template, then watch your own real-user monitoring for a few days. The Chrome UX Report and Search Console use a rolling 28-day window, so start validation in Search Console and expect the field figure to improve gradually over about four weeks.
Edge cases worth knowing
- The LCP is a cookie banner. A large consent notice with plenty of text can become the LCP element. That is fine if it appears quickly; if the consent script loads late, it can make LCP worse. Keep banner text short and load the consent tool early and lightly.
- Back and forward navigation. Pages restored from the back/forward cache report very
fast LCP in field data. Making your pages eligible for that cache, by avoiding
unloadhandlers andCache-Control: no-storewhere you can, genuinely improves the field figure. - Carousels. An auto-rotating slider can make the LCP a later slide, and slider scripts often hide slides until they initialise. Render the first slide as a plain image in the HTML and enhance it afterwards, or replace the carousel with one static hero.
- Video heroes. Always set a
posterimage that is small and fast, and preload that poster rather than the video file. - Different element on mobile. A hero hidden with
display: noneon small screens is often still downloaded. Use a<picture>element with media conditions so each screen size downloads only the image it shows.
Keeping LCP fast
LCP tends to drift back when a new hero section, a new slider or a new plugin arrives. A few habits stop that:
- Write the hero image rules into the template itself, so editors cannot switch lazy loading back on for the first section.
- Set a size budget for hero images and have your upload process create AVIF and WebP versions at fixed widths.
- Add a Lighthouse check to your deployment process that fails when LCP on a test page goes over an agreed limit, and keep real-user monitoring running so field regressions are noticed within days.
- Review any script that must run before the page is shown, such as A/B testing or personalisation tools, before it is added.
If your pages also move around as they load, the layout shift guide covers the related fixes, and the First Contentful Paint guide deals with the blocking files that delay the very first paint.
1. What is a good LCP score?
Google treats a Largest Contentful Paint of 2.5 seconds or less as good, between 2.5 and 4 seconds as needing improvement, and over 4 seconds as poor. The figure that counts is the 75th percentile of real visits over 28 days, measured separately for mobile and desktop. So at least three out of four page loads need to reach LCP within 2.5 seconds. A single fast lab test on your own computer does not show whether the page passes for your visitors.
2. How do I find out which element is my LCP element?
PageSpeed Insights and Lighthouse name the element in their diagnostics, and the Performance panel in Chrome DevTools highlights it when you record a page load. You can also paste a small PerformanceObserver script into the console that logs each LCP candidate as the page loads, with the final one being the element that counts. Check on a phone-sized screen too, because mobile layouts often use a different hero image or a large heading instead.
3. Should I lazy-load my hero image?
No. Lazy loading tells the browser to wait until it knows the image is near the viewport, which pushes the request later and directly lengthens LCP. Load the hero image and anything else in the first screenful eagerly, which is the default when there is no loading attribute, and give the single LCP image fetchpriority set to high. Keep lazy loading for images further down the page, where it saves data without affecting the first view.
4. Does fetchpriority high replace preload?
They solve different problems. fetchpriority on an image tag raises the priority of an image the browser has already found in the HTML. A preload link makes the browser aware of a resource it would otherwise find late, such as a CSS background image or an image inserted by JavaScript. If your hero is a normal image tag near the top of the HTML, fetchpriority is usually enough. If it is hidden from the HTML, you need a preload with high priority.
5. My LCP element is text, not an image. How do I speed it up?
A text element has nothing to download, so its LCP time is made up of server response time and render delay. Look at what stops the browser painting the text: large stylesheets in the head, synchronous scripts, and web fonts that hide text while they load. Inline the critical CSS, defer scripts, and set font-display to swap or optional for the heading font. Also make sure the text is in the server-sent HTML rather than rendered by JavaScript.
6. Why is my LCP fine in Lighthouse but poor in Search Console?
Lighthouse tests one simulated visit, while Search Console reports real visitors over 28 days. Real visitors may use slower phones, arrive through redirects from ads or emails, see a cookie banner, or be further from your server. The fix may also be too recent to show in the rolling window. Compare the field and lab breakdowns, add real-user monitoring to see which stage is slow for real visitors, and read our guide to CrUX and lab mismatches.
7. Will a CDN fix my LCP?
A CDN can help two of the four stages. Caching whole pages at the edge shortens time to first byte, and serving images from nearby servers shortens download time. It will not fix a hero image that is lazy-loaded, set by JavaScript or hidden behind blocking scripts, which are among the most common causes. Break your LCP into its stages first; if the delay stages are the largest, a CDN on its own will make little difference.
The Core Web Vitals checker shows real-user field data from the Chrome UX Report and a Lighthouse lab test side by side, breaks down LCP and points to the assets and code behind the failure.
Open Core Web Vitals Fixes โ