How to improve LCP: the fixes that move the number
·7 min read
To improve LCP, find out which of its four subparts is slow and fix only that one. The subparts are time to first byte, resource load delay, resource load duration and element render delay. On most slow pages the time goes to the server response and to the wait before the browser even requests the hero image, so caching HTML at a CDN, adding fetchpriority="high" and removing loading="lazy" from the LCP image beat compressing it again. The target is 2.5 seconds or less at the 75th percentile of real visits.
Skip the twenty-tip checklists. A page whose server takes two seconds to answer cannot get under 2.5 seconds however small its hero image is, so measure first and go to the section that matches.
What LCP measures and what counts as a good score
Largest Contentful Paint (LCP) is the time from the start of navigation until the largest image or text block in the viewport renders. These are Google's thresholds, from web.dev.
| 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 |
Google judges the 75th percentile of page loads, separately for mobile and desktop. If more than a quarter of mobile visits take longer than 4 seconds, the page is poor on mobile, whatever the median says.
The LCP element can be an image, a video's poster or first frame, a CSS url() background or a block of text. Chrome ignores invisible elements, full-viewport backgrounds and low-entropy placeholders, so a tiny blurred placeholder usually does not stop the clock.
Field data vs lab data: which LCP number counts
Field data is what real Chrome users experienced over the past 28 days, and it is the number Google's Core Web Vitals assessment uses. Lab data is one simulated load, and its job is to show you causes.
| Field data | Lab data | |
|---|---|---|
| Source | Chrome User Experience Report (CrUX) | Lighthouse |
| Who loads the page | Real visitors | A simulated mid-tier phone or desktop |
| Time window | Rolling 28 days, 75th percentile | One load, right now |
| Use it for | Deciding whether LCP is a problem | Finding out why |
PageSpeed Insights shows CrUX data for the URL when it has enough traffic, falls back to the whole origin when it does not, and shows nothing when the origin is too small as well. Safari 26.2 added the LCP API in December 2025, but CrUX still collects from Chrome users only, as web.dev confirms.
When the two disagree, believe the field. A green Lighthouse run next to poor field LCP often means real visitors hit something the lab skips, such as a redirect from an ad link, a cold connection or a slower phone.
LCP subparts: find the slow one first
Every LCP splits into four subparts that add up to the total. Web.dev suggests these rough shares.
| Subpart | What it measures | Healthy share |
|---|---|---|
| Time to first byte (TTFB) | Navigation start until the first byte of HTML | About 40% |
| Resource load delay | First byte until the browser starts fetching the LCP resource | Under 10% |
| Resource load duration | Downloading the LCP image or font | About 40% |
| Element render delay | Resource loaded until the element is on screen | Under 10% |
For a text LCP with no web font, both resource subparts are zero and everything after TTFB is render delay.
The best numbers on this come from an analysis of Chrome field data on web.dev. For origins with poor LCP, the median p75 TTFB was 2,270 ms and the median load delay was 1,290 ms, while downloading the image took 350 ms. Image weight is rarely the problem. Waiting is.
To see your own split in the lab, record a page load in the Performance panel of Chrome DevTools and open the LCP breakdown insight. Older Chrome versions label it LCP by phase. For real users, the attribution build of Google's web-vitals library reports the same four values.
import { onLCP } from 'web-vitals/attribution';
onLCP(({ value, attribution }) => {
// Send these to your analytics instead of the console
console.log(Math.round(value), attribution.target, {
ttfb: attribution.timeToFirstByte,
loadDelay: attribution.resourceLoadDelay,
loadDuration: attribution.resourceLoadDuration,
renderDelay: attribution.elementRenderDelay,
});
});How to improve LCP when TTFB is slow
Cache the HTML close to your visitors and remove redirects. Web.dev treats a TTFB of 0.8 seconds or less as good, and every millisecond of it counts toward the 2.5 second budget.
- Check whether your CDN caches HTML at all. Cloudflare does not cache HTML until a cache rule tells it to.
- Tell shared caches how long to keep the page.
s-maxageapplies to the CDN, andmax-age=0makes browsers revalidate. - Strip tracking parameters such as
utm_sourceandgclidfrom the cache key, or every ad click is a cache miss. - Cut redirect hops. An ad linking to
http://example.comthat redirects tohttps://and then towwwmakes the browser wait for two extra responses before the HTML. Our redirect chain checker shows every hop and the server rule to change.
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=0, s-maxage=300If the origin is slow on a cache miss, profile the queries behind the page. An overloaded origin eventually returns errors instead of slow pages, which our guide to the 502 Bad Gateway error covers.
How to improve LCP when resource load delay is slow
Put the LCP image in the HTML as a plain <img> and mark it as the one that matters.
<img src="/img/hero-1200.avif"
srcset="/img/hero-800.avif 800w, /img/hero-1200.avif 1200w"
sizes="100vw" width="1200" height="600"
alt="Trail runner crossing a ridge at sunrise"
fetchpriority="high">Chrome raises images in the viewport to high priority only after layout. fetchpriority="high" starts the request at high priority as soon as the browser finds the tag. In a test on Google Flights it cut LCP from 2.6 s to 1.9 s. Chrome and Edge 102, Firefox 132 and Safari 17.2 support it.
Never put loading="lazy" on the LCP image, because a lazy image is not requested until layout. Some themes and lazy-load plugins add it to every image, hero included. Script-based lazy loaders are worse, because the preload scanner cannot see a URL in data-src.
When the LCP image is a CSS background or gets inserted by a script, preload it in the <head> with high priority on the preload itself.
<link rel="preload" as="image" href="/img/hero-bg.avif" fetchpriority="high">For a responsive image, add imagesrcset and imagesizes to the preload so it fetches the same file the page uses. Serve the LCP image from the same origin as the HTML when you can, because a third-party image host costs a new connection.
Our image SEO checker flags a main image that is lazy-loaded, one that only a script can load, and one with no fetchpriority="high" or preload.
How to improve LCP when resource load duration is slow
Send fewer bytes for the LCP image, from a server near the visitor. Use srcset and sizes so phones never download the desktop width, and serve AVIF or WebP with a JPEG fallback.
<picture>
<source type="image/avif" srcset="/img/hero-800.avif 800w, /img/hero-1600.avif 1600w" sizes="100vw">
<source type="image/webp" srcset="/img/hero-800.webp 800w, /img/hero-1600.webp 1600w" sizes="100vw">
<img src="/img/hero-1600.jpg" width="1600" height="800" alt="Trail runner crossing a ridge at sunrise" fetchpriority="high">
</picture>fetchpriority goes on the <img>, since that element makes the request. Give versioned image files a long cache lifetime, such as cache-control: public, max-age=31536000, immutable. For a text LCP that waits on a web font, serve a subset WOFF2 file.
If load duration is already a few hundred milliseconds, more compression will barely move the field number.
How to improve LCP when element render delay is slow
Clear what blocks rendering and put the LCP content in the HTML the server sends. Web.dev names the usual causes. Stylesheets or synchronous scripts in the <head> are still loading, the element waits for JavaScript to add it, an A/B testing script hides the page while it picks a variant, or long tasks hold the main thread.
<!-- Blocks rendering until app.js downloads and runs -->
<script src="/js/app.js"></script>
<!-- Downloads in parallel and runs after parsing -->
<script src="/js/app.js" defer></script>Remove unused rules from the blocking stylesheet first. Inline CSS only when it is small, because inlined CSS cannot be cached.
If client-side JavaScript creates your hero heading or <img> tag, LCP waits for the bundle to download and run. Server-render or statically generate that part of the page. Our AI Crawler View compares the raw HTML with the rendered page, so you can see whether the hero exists before any script runs.
For a text LCP, keep the font from hiding the text.
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<style>
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2");
font-display: swap;
}
</style>Any font-display value except auto or block paints the text in a fallback font instead of waiting for the file.
LCP fixes people over-apply
These get applied by reflex, and past a point none of them changes LCP.
- Recompressing the hero. If a stylesheet or script still blocks rendering when the image arrives, a smaller file only moves the waiting into render delay.
- High priority on everything. Per web.dev, high priority on more than one or two images makes it unhelpful for LCP.
- Adding
loading="eager". Eager is the default, so it does nothing. The fix is removinglazy. - Chasing a perfect desktop Lighthouse score. If CrUX already shows good LCP at the 75th percentile, the LCP work is done.
Check your LCP in field and lab data
Our Core Web Vitals Snapshot reads Google's PageSpeed Insights for one public URL, mobile or desktop. It returns real-user LCP, INP and CLS at the 75th percentile from CrUX, for the URL or, failing that, the whole origin, with a pass or fail against Google's thresholds. Beside that it shows a Lighthouse lab run with the performance score, LCP, CLS, FCP, Total Blocking Time, server response time and Speed Index, plus up to eight Lighthouse opportunities ranked by estimated savings.
When CrUX has no record for the URL or the origin, the snapshot says so and rates the page on the lab run alone, marked as a debugging signal rather than the number Search uses. It does not split LCP into subparts, so use the DevTools breakdown once the snapshot shows LCP is the metric to fix. Each run checks one URL and costs 20 credits.