Evaluat is in private access. Demos open through July. Book a slot

Blog Web Vitals & Metrics

How to improve LCP: a step-by-step optimization guide

Most teams trying to improve Largest Contentful Paint reach for a smaller image, and most of the time the image is not the problem. The browser is starting it too late. This guide is the practical playbook: the fixes in priority order, the code for each, the two 2025 techniques most sites have not adopted, and how to confirm the number actually moved for real users under load.

Written by: Ahmad Farzan ·

Two Largest Contentful Paint timelines compared. Before optimization the resource load delay phase is large and LCP lands at 4.3 seconds, rated poor. After starting the LCP image early the load delay nearly disappears and LCP drops to 2.3 seconds, under the 2.5 second good threshold.

To improve Largest Contentful Paint, find the element that paints last, split its time into four phases, and fix the biggest phase first. On most pages the biggest phase is not a heavy image. It is a late start, where the browser does not begin loading the LCP image early enough. As of the 2025 Web Almanac, only 62% of mobile pages have a good LCP, so close to four in ten ship a hero that arrives too slowly.

This guide is the practical playbook: the fixes in priority order, the code for each, the two 2025 techniques most teams have not adopted yet, and how to confirm the number actually moved for real users. If you want the concept first, what counts as the LCP element and what the four phases are, start with Largest Contentful Paint, explained. This is the how, not the what.

First, find the element and split its time

Every LCP fix starts with two measurements, not guesses: which element is the LCP element, and which of its four phases owns the most time. Optimizing the wrong element, or the right element’s wrong phase, is the most common way to spend a day and move nothing.

Find the element first. Chrome DevTools names it for you: open the Performance panel, record a page load, and the LCP marker points at the exact node. For a quick reading without DevTools, you can test your page speed for free with Evaluat Pulse, which loads the page in real Chrome, reports LCP alongside FCP and TTFB, and records the load so you can watch the moment the element paints. Across the web 76% of mobile pages have an image as their LCP element (2025 Web Almanac), so most LCP work lands on image delivery, but confirm yours before you touch anything.

Then split the time. LCP is the sum of four sequential phases: Time to First Byte, resource load delay, resource load duration, and element render delay. web.dev’s ideal split is roughly 40% TTFB, under 10% load delay, about 40% load duration, and under 10% render delay (web.dev, 2025). The full breakdown lives in the LCP explainer; what matters here is the order of attack. And the order is rarely what people expect, because most origins with poor LCP spend less than 10% of their LCP time downloading the image, with the image instead delayed on the client by about 1,290 milliseconds at the 75th percentile (web.dev, 2024). The download is usually a thin slice. The late start is the bloat.

The fixes below are ordered by where the time usually hides.

Fix, in orderThe phase it attacksThe changeEffort vs payoff
Start the LCP image immediatelyResource load delayDrop loading="lazy", add fetchpriority="high", put it in the HTMLLow effort, usually the biggest win
Cut Time to First ByteTTFBCache HTML, serve from the edgeMedium effort, gates everything else
Shrink the image downloadResource load durationAVIF or WebP, srcset, a CDNMedium effort, only after the load delay is gone
Clear what blocks the paintElement render delayDefer non-critical JS, inline critical CSSMedium effort, small but real
Prerender the next pageThe whole timeline, next navigationSpeculation Rules plus bfcacheLow effort, near-instant repeat navigation

Step 1: make the LCP image start loading immediately

This is the cheapest, highest-leverage fix, and the one most sites miss. The goal is simple: the browser should discover and request the LCP image as early as possible, in the first pass over the HTML. Get this right and a one or two second load delay can collapse toward zero with no change to the image itself.

Three rules do most of the work. Reference the image in the initial HTML response so the browser’s preload scanner finds it, never lazy-load it, and mark it high priority:

<!-- In the server-rendered HTML, not injected later by JavaScript -->
<img
  src="/hero.avif"
  width="1280" height="720"
  fetchpriority="high"
  decoding="async"
  alt="A clear description of the hero image" />

The numbers say how much room there is here. Roughly 16 to 17% of pages still lazy-load their LCP image, and only 17% of mobile pages with an image LCP use fetchpriority, up from 15% the year before (2025 Web Almanac). Both are self-inflicted late starts.

If the image is set in CSS or injected by a script, the preload scanner cannot see it, so add an explicit preload in the <head>:

<link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />

On a framework, use the built-in hint rather than hand-rolling it. Next.js exposes priority on next/image (<Image src={hero} alt="..." priority />), which sets eager loading and high fetch priority together; Astro’s <Image> takes loading="eager". Verify it worked in the DevTools network waterfall: the LCP image request should begin near the very start, not after a stack of scripts.

Step 2: cut Time to First Byte

TTFB is the first phase and, on a well-tuned page, the largest, around 40% of LCP. Nothing downstream can start until the first byte of HTML arrives, so this phase gates the other three. If your server takes 800 milliseconds to respond, no amount of image work gets you under 2.5 seconds.

The levers are familiar. Cache the HTML and your API responses so a request does not rebuild the page from scratch. Serve from an origin or CDN edge in the region closest to your customers, so the bytes travel a short distance. And remove slow synchronous work from the request path: an uncached database query, a third-party call, a cold serverless function.

One caveat that single-user testing hides: TTFB is the phase that gets worse under load. As concurrency rises the server queues requests, and the first byte that arrived in 200 milliseconds for one idle visitor can take a second or more at peak. A green TTFB in a quiet lab is not a green TTFB on sale day, which is why the final step is to measure under realistic load, not just once.

Step 3: shrink the image download

Once the request starts on time, make the download quick. This is the classic “optimize your images” advice, and it is real, but it belongs third, not first, because it only helps after the load delay is gone.

Serve a modern format and size the image to the device. Converting to AVIF or WebP is the single biggest file-size win: in DebugBear’s testing on the MDN blog, an LCP image dropped 95%, from 1 MB to 46 KB, on conversion to AVIF (MDN, 2025). Then use responsive srcset so phones do not download desktop pixels:

<img
  src="/hero-1280.avif"
  srcset="/hero-640.avif 640w, /hero-1280.avif 1280w, /hero-1920.avif 1920w"
  sizes="(max-width: 700px) 100vw, 1280px"
  width="1280" height="720"
  fetchpriority="high"
  alt="A clear description of the hero image" />

Always set width and height (or an aspect ratio) so the browser reserves space and the image does not shift layout in late, and serve it from a CDN so the bytes start close to the user. Keep the encoded image roughly matched to its display size; a 4000-pixel-wide photo in an 800-pixel slot is wasted download.

Step 4: clear what blocks the paint

The element has arrived but has not painted. The render delay should be under 10% of LCP, and when it is not, the cause is almost always render-blocking resources in the <head> or fonts that reflow text after it appears.

Three fixes cover most cases. Defer non-critical JavaScript with defer or type="module" so scripts stop blocking the parser. Inline the small amount of CSS needed to render the top of the page and load the rest asynchronously, so the browser is not waiting on a full stylesheet to paint the hero. And set font-display: swap on web fonts so text paints immediately in a fallback face instead of staying invisible while the font downloads. None of these change the LCP element itself; they remove the things standing between its bytes and the screen.

Step 5: make the next navigation instant

The first four steps speed up the current page. The newest lever speeds up the next one, and it is the technique most teams have not turned on yet. The Speculation Rules API lets Chrome prerender a page the user is likely to visit, in the background, before they click. When they do click, the page is already rendered, so it appears almost instantly and its LCP is effectively zero.

You declare the rules in a small script tag, with an eagerness setting that trades coverage against server cost:

<script type="speculationrules">
{
  "prerender": [{
    "where": { "href_matches": "/*" },
    "eagerness": "moderate"
  }]
}
</script>

The payoff is documented. Monrif cut LCP by up to 17.9% and lifted engagement by up to 8.9% by pairing Speculation Rules prerendering with back/forward cache support (web.dev, 2025). The back/forward cache (bfcache) is the companion win: it restores a fully rendered page from memory on back and forward navigations, with a sub-second LCP, as long as you avoid unload listeners. Start with moderate eagerness on safe, idempotent links, watch your server load, and widen from there.

Verify the fix held: at the 75th percentile, and under load

A fix you cannot measure is a guess. After each change, confirm the number moved where it counts: in the field, at the 75th percentile, and under realistic concurrency.

Read field LCP with Google’s web-vitals library, which wraps the browser’s native measurement, and report it at the 75th percentile rather than the mean, because the published threshold is the 75th percentile (web.dev, 2025) and an average hides the slow tail:

import { onLCP } from 'web-vitals';

onLCP(({ value, rating }) => {
  // send value to your analytics; aggregate at p75, not as an average
  console.log(value, rating); // e.g. 2180, "good"
});

Two numbers will disagree, and that is expected: lab tools run one visitor on a quiet server, while field data reflects real devices, networks, and a busy origin. Core Web Vitals: lab vs field explains which to trust for what. The dimension a single lab run cannot reproduce is concurrency, and it is the one that bites in production, because TTFB swells as load rises. Re-running the page in a real browser at realistic load is how you catch an LCP that is fine for one user and poor at peak; that is the subject of measuring Core Web Vitals at load. To stop a fix from quietly regressing later, wire the check into CI as a budget, covered in performance regression testing.

Common mistakes when improving LCP

The same handful of errors waste most LCP effort. Each has a one-line fix.

  • Optimizing the wrong element. Swapping the hero does nothing if the LCP node is a heading. Confirm the element in DevTools before you touch it.
  • Lazy-loading the LCP image. loading="lazy" on the element that should paint first guarantees a late start. Lazy-load below the fold, never the LCP.
  • Compressing the image before fixing the load delay. A smaller file that still starts loading two seconds in is still slow. Fix the start time first, the size second.
  • Preloading a second large image. A preload that competes with the real LCP image for bandwidth slows the one that matters. Preload the LCP image only, and only when it is discovered late.
  • Testing one idle user in the lab. A 1.8-second Lighthouse score says little about the 4-second LCP your users get at peak. Lab has no concurrency.
  • Reporting the average. The mean flatters you by hiding the slow quarter. Track and gate on the 75th percentile.

Where Evaluat fits

Most of the steps above you can do with free tools: DevTools to find the element and split the phases, Evaluat Pulse for a quick single-page reading in real Chrome, Lighthouse or DebugBear for a lab audit. Use them; they are the right tools for a single page on a quiet server.

Evaluat is built for the part those miss: what your LCP does when real traffic arrives. Each virtual user runs in its own isolated browser, so the LCP it records is the browser’s native value, captured while hundreds of users hit the site at once. When LCP crosses 2.5 seconds at peak, the per-session network log shows exactly when the image was requested and how long it took, and the session video shows the moment it painted, so a late start and a slow download read as the two different problems they are. How it works covers the architecture.

One honest boundary: if you are load-testing a pure API with no page to render, a protocol tool like k6 is the lighter, cheaper fit. Evaluat earns its place when the question is what a real browser does under load, not whether a server returns 200s.

Improving LCP is not a single trick. Find the element, split it into four phases, and fix the earliest, largest one first, which is usually the load delay and not the image. Then prove the fix held in the field, at the 75th percentile, under the load your users actually bring.

Test in real browsers. Debug in real sessions. Book a demo.

Ahmad Farzan, Founder at Evaluat

About the author

Ahmad Farzan · Founder at Evaluat

Founder of Evaluat. Has spent years building and load-testing Adobe Commerce and Magento storefronts, and built Evaluat to test sites the way real browsers actually hit them.

More from Ahmad →

Common questions

FAQ

What is the fastest way to improve LCP?

Remove the resource load delay on the LCP image. Reference it directly in the initial HTML, never put loading="lazy" on it, and add fetchpriority="high". On most pages the LCP image starts loading far too late, so this single change often does more than any amount of image compression, and it takes minutes.

Why did my LCP not improve after I optimized the image?

Because the download was probably never the bottleneck. web.dev reports that most origins with poor LCP spend less than 10% of their LCP time downloading the image; the time is lost earlier, in a slow first byte or a late start on the image request. Split your LCP into its four phases and fix the largest one, which is usually not the download.

What is a good LCP score to aim for?

Good is 2.5 seconds or less at the 75th percentile of real page loads; above 4 seconds is poor. The 75th percentile matters because a fast average can still hide a slow quarter of visits, usually on weaker devices or at peak traffic, and that quarter is where users give up.

Does preloading the image improve LCP?

It can, when the image is discovered late. A tells the browser to fetch it sooner, which cuts the load delay. But fetchpriority="high" on the image element is usually simpler and enough. Preload the LCP image only, never a second large image, or you will slow the real one down.

Can a CDN or caching improve LCP?

Yes, on two phases. Caching the HTML and serving it from an edge close to your users cuts Time to First Byte, the first and often largest phase of LCP. Serving the LCP image from the same CDN shortens its download. Both help, and TTFB gains matter most because nothing downstream can start until the first byte arrives.

Does prerendering improve LCP?

Dramatically, on the prerendered navigation. The Speculation Rules API lets Chrome render a likely next page in the background, so when the user clicks it appears almost instantly and LCP is effectively zero. Monrif combined prerendering with back/forward cache and cut LCP by up to 17.9% (web.dev, 2025). It helps repeat and onward navigations, not the very first load.

How do I confirm LCP improved for real users?

Lab tools confirm a fix in principle; field data confirms it in practice. Read your LCP at the 75th percentile from the Chrome User Experience Report or your own real user monitoring, not as an average, and measure it under realistic concurrency, because LCP rises under load as the server queues requests.

See it on your site

Test in real browsers.
Debug in real sessions.

Want to see this measured on your app?

30 minutes. We build a scenario on your real customer journey, run a small test, and walk you through the report with your data in it.