LCP: Optimizing the Largest Contentful Paint

LCP is the metric that most directly reflects what a user experiences as "the page loading." The moment the largest element in the viewport is fully rendered — whether that's a hero image, a large heading, or a full-bleed background — is the moment the user perceives the page as ready. Getting this under 2.5 seconds at the 75th percentile is the first, most visible performance goal for any page.

The challenge is that LCP sits at the intersection of networking, server performance, rendering priority, and image delivery. A slow LCP can have a dozen different root causes, and fixing the wrong one wastes time. The right approach is to decompose LCP into its contributing phases, measure each one, and target the largest contributor first.

Decomposing LCP Into Its Phases

LCP doesn't happen in one step. It's the sum of four distinct phases:

  1. Time to First Byte (TTFB): Time from the browser starting navigation to receiving the first byte of the HTML document.
  2. Resource load delay: Time from TTFB until the browser discovers and starts downloading the LCP resource (image, font, or text). This is heavily influenced by whether the resource is in the initial HTML or requires JavaScript to discover.
  3. Resource load time: The actual download time for the LCP element, if it's an image or other external resource.
  4. Element render delay: Time from resource download completing until the browser paints the element. Usually caused by render-blocking CSS or JavaScript.

Chrome DevTools exposes all four phases in the Performance panel. Load the page with recording active, then find the "LCP" marker in the Timings row. Click it to see a breakdown. Alternatively, the web-vitals JavaScript library reports LCP attribution data in real user sessions, including which element was the LCP candidate and which phase was slowest.

Start by identifying which phase is dominating. The fixes are completely different depending on the answer.

Reducing TTFB: The Foundation

A target TTFB under 600ms is reasonable for most global audiences. If your TTFB is 1.5 seconds, no amount of image optimization will get LCP under 2.5 seconds — you've already burned most of your budget before the browser even starts fetching the image.

The most impactful TTFB levers are hosting architecture and caching:

Edge hosting: Deploying on Cloudflare Workers, Vercel Edge, or similar platforms places your server logic geographically close to users, dramatically reducing round-trip latency. A user in Tokyo hitting an origin server in Virginia adds ~150ms of latency before the first byte even arrives. Edge functions eliminate this by executing at a node near the user.

HTTP/3 (QUIC): HTTP/3 reduces connection overhead, especially on lossy mobile networks where TCP's head-of-line blocking causes significant delays. Enable it at the CDN or load balancer level — most major CDNs support it as a configuration toggle.

Full-page caching with stale-while-revalidate: For pages that don't require per-request server rendering, serving from cache is orders of magnitude faster than generating a response. The Cache-Control: stale-while-revalidate=3600 directive tells the CDN to serve stale content immediately while refreshing in the background, eliminating cache miss penalties.

Cache-Control: public, max-age=60, stale-while-revalidate=3600

Preconnect to critical origins: If your LCP image is hosted on a CDN subdomain or third-party image service, the browser needs to do DNS lookup, TCP handshake, and TLS negotiation before it can request it. A preconnect hint initiates these steps as early as possible:

<link rel="preconnect" href="https://images.example-cdn.com" crossorigin />

Image Format: AVIF First

For image-based LCP elements — which covers most landing pages, articles with hero images, and product pages — the image format and delivery pipeline directly determine resource load time.

AVIF is the right default in 2026. It consistently produces files 30–50% smaller than WebP at equivalent visual quality, and browser support is now broad across Chrome, Firefox, Safari, and Edge. That size reduction translates directly to faster download time, particularly on constrained mobile connections.

Use <picture> with a fallback to WebP for any remaining browsers:

<picture>
  <source srcset="hero.avif" type="image/avif" />
  <source srcset="hero.webp" type="image/webp" />
  <img
    src="hero.jpg"
    alt="Product launch"
    width="1200"
    height="630"
    fetchpriority="high"
    loading="eager"
  />
</picture>

Combine this with responsive srcset so browsers only download the resolution they need:

<img
  srcset="hero-480w.avif 480w, hero-800w.avif 800w, hero-1200w.avif 1200w"
  sizes="(max-width: 600px) 480px, (max-width: 900px) 800px, 1200px"
  src="hero-1200w.avif"
  alt="Hero image"
  width="1200"
  height="630"
  fetchpriority="high"
  loading="eager"
/>

fetchpriority="high": The Single Most Impactful Attribute

Of all the LCP optimizations available, adding fetchpriority="high" to the LCP image is consistently the highest-impact single change you can make. Without it, the browser uses its own heuristics to prioritize resource downloads, and it often gets it wrong — especially when there are many images on the page.

fetchpriority="high" is an explicit signal to the browser's resource scheduler: download this before anything else in the network queue. On pages with multiple images, carousels, or third-party scripts competing for bandwidth, this can shave hundreds of milliseconds off LCP.

Three rules for fetchpriority:

  1. Apply it to the LCP element only. Marking everything as high-priority means nothing is.
  2. Never combine it with loading="lazy" — these attributes directly conflict.
  3. The LCP element should always be loading="eager" (or omit loading entirely, since eager is the default).
<img src="hero.avif" alt="Hero" width="1200" height="630" fetchpriority="high" loading="eager" />

Preloading the LCP Resource

Even with fetchpriority="high", the browser can only prioritize the image once it discovers it in the HTML. If your LCP image is deep in the document, or loaded as a CSS background image, the browser might not encounter it until well into parsing. A <link rel="preload"> in the <head> fixes this by telling the browser about the resource before it parses the body:

<head>
  <!-- Preload the LCP image — place as early in <head> as possible -->
  <link
    rel="preload"
    as="image"
    href="/images/hero.avif"
    imagesrcset="/images/hero-480w.avif 480w, /images/hero-1200w.avif 1200w"
    imagesizes="(max-width: 600px) 480px, 1200px"
    fetchpriority="high"
  />
</head>

The imagesrcset and imagesizes attributes on preload links (available in modern browsers) ensure the browser preloads the correct responsive variant rather than always fetching the largest one.

Important: Preload only the LCP resource. Preloading additional resources competes for bandwidth and can actually delay LCP. Audit any existing preloads on your page — accidental over-preloading is a common source of LCP regression.

Eliminating Render-Blocking Resources

Even after the LCP image has downloaded, the browser won't paint it until it has finished building the CSSOM and executing any synchronous JavaScript. Render-blocking CSS and JS add directly to the element render delay phase of LCP.

Inline critical CSS: Identify the CSS rules needed to render above-the-fold content — the LCP element, its container, basic layout — and inline them in a <style> block in <head>. Load the full stylesheet asynchronously:

<head>
  <style>
    /* Critical CSS for above-the-fold content only */
    .hero {
      width: 100%;
      aspect-ratio: 16/9;
    }
    .hero img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  </style>
  <!-- Non-critical CSS loads without blocking render -->
  <link
    rel="preload"
    href="/styles/main.css"
    as="style"
    onload="this.onload=null;this.rel='stylesheet'"
  />
  <noscript><link rel="stylesheet" href="/styles/main.css" /></noscript>
</head>

Tools like Critical or the CSS extraction built into Next.js automate critical CSS generation.

Remove unused CSS: If your stylesheet is large, tools like PurgeCSS or UnCSS can strip unused rules, reducing the time to parse and apply styles. A 200KB stylesheet takes meaningfully longer to process than a 30KB one.

Defer non-essential JavaScript: Scripts loaded with <script src="..."> block HTML parsing. Use defer for scripts that need DOM access and async for independent utilities. Never load analytics, tag managers, or chat widgets synchronously.

<!-- Blocks parsing — avoid for non-critical scripts -->
<script src="/vendor/heavy-library.js"></script>

<!-- Deferred: executes after HTML is parsed, preserves execution order -->
<script src="/app.js" defer></script>

<!-- Async: executes as soon as downloaded, doesn't preserve order -->
<script src="/analytics.js" async></script>

Diagnosing LCP Regressions in CI

LCP scores change when code changes. Catching regressions before they reach production requires integrating Lighthouse into your CI pipeline. The lighthouse-ci package provides a CLI and a GitHub Actions integration that fails builds when performance budgets are exceeded:

# .github/workflows/performance.yml
- name: Lighthouse CI
  run: |
    npm install -g @lhci/cli
    lhci autorun --upload.target=temporary-public-storage
  env:
    LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

Set performance budgets in lighthouserc.json:

{
  "ci": {
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["warn", { "maxNumericValue": 2500 }]
      }
    }
  }
}

Note that Lighthouse runs in a simulated lab environment, so its LCP values won't match CrUX field data exactly. Use Lighthouse for regression detection and CrUX (via PageSpeed Insights or Search Console) for ranking-relevant measurement.

The LCP Optimization Checklist

When approaching an LCP problem systematically, work through these in order:

  1. Measure CrUX field data in Search Console to confirm LCP is actually failing the 75th-percentile threshold
  2. Use DevTools Performance panel to identify which LCP phase (TTFB, load delay, load time, render delay) is dominant
  3. If TTFB is over 600ms: evaluate edge hosting, enable HTTP/3, implement full-page caching
  4. Identify the actual LCP element with Chrome DevTools or the web-vitals library
  5. If it's an image: convert to AVIF, add fetchpriority="high", add loading="eager", add a <link rel="preload"> in <head>
  6. Add responsive srcset to avoid serving oversized images
  7. Inline critical CSS, defer non-critical stylesheets, defer non-essential JS
  8. Audit preconnect hints for image CDN origins
  9. Verify with PageSpeed Insights — check field data, not just lab score