OctaneAudit
Core Web Vitals

How to Improve LCP Below 2.5 Seconds: A Step-by-Step Playbook

A repeatable process for fixing Largest Contentful Paint: find the element, measure the four sub-parts, then apply the fix that matches the phase you are losing time in.

OCOctaneAudit Team7 min read
Core Web Vitals

How to Improve LCP Below 2.5 Seconds: A Step-by-Step Playbook

Largest Contentful Paint is the metric most sites fail, and the one with the clearest path to passing. On most sites the slow part is not one mystery bottleneck but one or two phases of loading that can be measured and fixed directly.

This is the playbook we hand to engineering teams.

Step 1: Identify the LCP element per template

Do not guess. Open Chrome DevTools, record a Performance trace with mobile throttling, and look for the LCP marker in the Timings track. Click it and DevTools highlights the element. Alternatively, run PageSpeed Insights and expand "Largest Contentful Paint element" in the diagnostics.

On most sites the LCP element is one of:

  • A hero image or banner (homepage, landing pages).
  • The primary product image (product pages).
  • A heading or first paragraph (articles, text-heavy pages).
  • A background image set in CSS (older themes).

Record the element for each template. The fix is different for each type.

Step 2: Break LCP into its four sub-parts

Use the web-vitals/attribution library in the field, or the LCP breakdown in PageSpeed Insights:

Sub-partHealthy shareWhat it tells you
Time to First Byte~40%Server and network
Resource load delay< 10%Discovery: how late the browser learns about the resource
Resource load duration~40%Size and delivery of the resource
Element render delay< 10%Blocking: what stops it painting once loaded

Whichever phase is oversized is the one to attack first. The rest of this article is organized by phase.

Step 3a: Reduce Time to First Byte

TTFB is the floor under LCP. If your TTFB is 1.5 seconds, your LCP cannot be under 2.5 unless everything else is perfect.

  • Cache HTML at the edge. For pages that are the same for anonymous users (home, categories, products, articles), set Cache-Control: public, s-maxage=300, stale-while-revalidate=3600 and let your CDN serve them. This single change often cuts TTFB from over 1 second to under 200 milliseconds.
  • Use a CDN with edge locations near your customers. Serving US traffic from a single EU origin adds 100 to 200 milliseconds of latency before any work happens.
  • Fix slow origin responses. Profile the request. Common culprits are N+1 database queries, uncached API calls to third parties, and synchronous rendering of large component trees.
  • Enable HTTP/2 or HTTP/3 and Brotli compression.
  • Send 103 Early Hints for the critical stylesheet and the LCP image so the browser starts fetching them while the server finishes the HTML.
HTTP/1.1 103 Early Hints
Link: </css/critical.css>; rel=preload; as=style
Link: </img/hero-1024.avif>; rel=preload; as=image; fetchpriority=high

Step 3b: Eliminate resource load delay

Load delay means the browser knew about the page long before it knew about the LCP image. Causes, in order of frequency:

  1. The image is lazy-loaded. loading="lazy" on an above-the-fold image delays the request until layout completes. Remove it from the LCP candidate.
  2. The image is a CSS background. The browser only discovers it after parsing CSS and matching the selector. Either move it to an <img> or preload it.
  3. The image is injected by JavaScript. Sliders, React image components that render a placeholder first, or A/B tools. Make sure the real src is in the server HTML.
  4. Render-blocking resources sit ahead of it. The preload scanner can find the image, but large synchronous scripts in <head> delay everything.

The fix for all four:

<link rel="preload" as="image" href="/img/hero-1024.avif"
      imagesrcset="/img/hero-640.avif 640w, /img/hero-1024.avif 1024w, /img/hero-1600.avif 1600w"
      imagesizes="100vw" fetchpriority="high">

<img src="/img/hero-1024.avif"
     srcset="/img/hero-640.avif 640w, /img/hero-1024.avif 1024w, /img/hero-1600.avif 1600w"
     sizes="100vw" width="1600" height="720"
     fetchpriority="high" decoding="async" alt="...">

fetchpriority="high" tells Chrome to request the image before other images and most scripts. It is the single highest-leverage attribute for LCP.

Step 3c: Shrink resource load duration

This is the phase everyone optimizes first, and it is often not the biggest one. Still, oversized images are endemic.

  • Right-size. Serve the image at the width it is rendered. A 4000-pixel hero on a 390-pixel phone is 100 times more pixels than needed. Use srcset with 3 to 5 widths.
  • Modern formats. AVIF is typically 30 to 50% smaller than WebP, which is 25 to 35% smaller than JPEG. Use <picture> with fallbacks if you must support very old browsers.
  • Quality. Export at quality 70 to 80. Above that, size grows faster than perceptible quality.
  • CDN image transforms. Cloudflare Images, Imgix, Cloudinary or your framework's image component can do all of the above on the fly.
  • Keep the LCP image on the same origin or preconnect to the image host: <link rel="preconnect" href="https://cdn.example.com">.

Target: the LCP image should be under 150 KB on mobile, ideally under 100 KB.

When the LCP element is text

If LCP is a heading, load duration is about the web font. Either preload the font file, use font-display: swap so text paints in the fallback immediately, or use a system font for the hero heading.

Step 3d: Remove element render delay

The image has arrived but does not paint. Something is blocking the main thread or the element is hidden.

  • Render-blocking CSS. Inline the critical CSS for the above-the-fold region and load the rest with media="print" onload="this.media='all'" or a build-time critical CSS tool.
  • Synchronous JavaScript in <head>. Add defer to everything that is not needed for first paint. Tag managers and chat widgets are the usual offenders.
  • Hydration hides the hero. Some frameworks render the hero with opacity: 0 until a fade-in animation triggers after hydration. The LCP is recorded when it becomes visible, so the animation delays LCP by the entire JavaScript load. Render it visible first.
  • Client-side rendering. If the hero only exists after React mounts, LCP waits for your whole bundle. Server-render it.

Step 4: Verify, ship, measure

  1. Re-run the Performance trace and confirm the LCP marker moved and the sub-parts rebalanced.
  2. Ship to production behind the template, not a single page.
  3. Watch the 28-day trend in Search Console. Expect the 75th percentile to move within a week and the status to flip within a month.

A worked example

A fashion retailer came to us with a 3.9 second mobile LCP on collection pages. Breakdown: TTFB 1.1 s, load delay 0.5 s, load duration 1.8 s, render delay 0.5 s.

  • TTFB: enabled edge caching of collection HTML for 5 minutes. TTFB to 0.3 s.
  • Load delay: removed loading="lazy" from the first product image and added fetchpriority="high". Delay to under 0.1 s.
  • Load duration: switched from 2400-pixel JPEGs to 800-pixel AVIF via the platform's CDN. Duration to 0.6 s.
  • Render delay: deferred the heat-mapping script. Delay to 0.2 s.

Result after 4 weeks: mobile LCP 1.9 seconds at p75, collection pages "Good" in Search Console, and a 7% lift in add-to-cart rate on mobile.

Checklist

  • LCP element identified per template
  • Four sub-parts measured in the field
  • HTML cached at the edge, TTFB under 800 ms
  • LCP image in server HTML, not lazy-loaded, fetchpriority="high"
  • Right-sized AVIF/WebP under 150 KB with srcset
  • Critical CSS inlined, scripts deferred
  • No opacity or hydration gate on the hero
  • Verified in lab, trend watched in Search Console
SharePostLinkedInFacebook

Get Your SEO Audit

Every issue in this article, checked on your site by an auditor and delivered as a prioritized roadmap within 48-72 hours.

See pricing

Related articles