---
title: "LCP Optimization Best Practices 2026: The Complete Guide"
description: "The 2026 LCP playbook: Priority Hints, Speculation Rules, 103 Early Hints, Lighthouse 13 Insights, View Transitions, AVIF, HTTP/3, and edge streaming SSR."
canonical_url: "https://unlighthouse.dev/learn-lighthouse/lcp/best-practices-2026"
last_updated: "2026-04-13"
---

<audit-impact metric="lcp">

In 2026, optimizing Largest Contentful Paint means pairing `fetchpriority="high"` on the LCP element with Speculation Rules prerendering, 103 Early Hints for critical preloads, AVIF images, HTTP/3 with BBR, and edge streaming SSR. Lighthouse 13 Insights pinpoint resource discovery and render delay so fixes land where they move the 75th percentile.

<interactive-lcp-timeline>

The rules changed. [Lighthouse 13](https://github.com/GoogleChrome/lighthouse/releases/tag/v13.0.0) (released October 2025) replaced individual "Opportunities" with a consolidated **Insights panel**. [Speculation Rules API](https://developer.chrome.com/docs/web-platform/prerender-pages) landed in Chrome stable, becoming the primary way to achieve sub-100ms LCP. The old playbook of preload tags and `font-display: swap` still works, but it leaves seconds on the table.

This guide covers every 2026 optimization that moves CrUX data.

## Priority Hints are baseline

`fetchpriority="high"` is no longer optional on the LCP element. [Every major browser engine now supports it](https://caniuse.com/mdn-html_elements_img_fetchpriority). [Chrome's priority fetch](https://web.dev/articles/fetch-priority) lifts the LCP image above render-blocking CSS in the network queue.

```html
<img
  src="/hero.avif"
  fetchpriority="high"
  decoding="async"
  width="1200"
  height="600"
  alt="Hero"
>
```

<tip>

One `fetchpriority="high"` per page. Set it on whatever element CrUX reports as LCP, usually the hero image or an H1 with a background image.

</tip>

Drop `fetchpriority="low"` on below-the-fold heroes, carousel slides 2+, and embedded video posters. This [frees bandwidth for the real LCP candidate](https://web.dev/articles/fetch-priority#optimize_lcp).

```html
<img src="/carousel-2.avif" fetchpriority="low" loading="lazy" alt="">
<img src="/carousel-3.avif" fetchpriority="low" loading="lazy" alt="">
```

Audit with the [LCP Finder tool](/tools/lcp-finder) to confirm which element browsers pick.

## Speculation Rules prerendering

[Speculation Rules](https://developer.chrome.com/docs/web-platform/prerender-pages) let you prerender the next page before the user clicks. LCP on the prerendered page is near-zero because the browser finished painting before navigation started.

```html
<script type="speculationrules">
{
  "prerender": [{
    "where": {
      "and": [
        { "href_matches": "/*" },
        { "not": { "href_matches": "/logout" } },
        { "not": { "selector_matches": "[data-no-prerender]" } }
      ]
    },
    "eagerness": "moderate"
  }]
}
</script>
```

Eagerness levels have been refined for 2026:

- **immediate**: Prerenders as soon as the browser parses the rule.
- **eager**: Triggers on "hint of intent," such as scrolling the link into the viewport.
- **moderate**: Waits for hover (typically 200ms) or pointer proximity.
- **conservative**: Waits for `pointerdown`.

Start with `moderate`. It balances LCP wins against bandwidth. [Chrome typically limits active prerenders to 2 concurrent pages](https://developer.chrome.com/docs/web-platform/prerender-pages#limitations) to preserve device resources.

<warning>

Exclude logout, cart mutations, and tracking pixels. Anything with side effects needs `data-no-prerender`.

</warning>

## Lighthouse 13 Insights panel

[Lighthouse 13](https://developer.chrome.com/docs/lighthouse/overview) consolidated legacy audits into three primary LCP-specific insights:

<lighthouse-devtools-mockup>

**LCP request discovery (lcp-discovery-insight).** Replaces the old `lcp-lazy-loaded` and `prioritize-lcp-image` audits. It identifies LCP images missing from the initial HTML or marked with `loading="lazy"`. Fix by putting the image tag in the server HTML or using a `<link rel="preload">` hint.

**LCP render delay (lcp-render-delay-insight).** Measures the gap between resource load complete and paint. High render delay means render-blocking CSS or JavaScript, or long main thread tasks like hydration blocking the compositor.

**LCP breakdown (lcp-phases-insight).** Shows the four-part lifecycle: TTFB, resource load delay, resource load duration, and element render delay. Target under 10% for both load delay and render delay.

```bash
npx lighthouse https://your-site.com \
  --output=json \
  | jq '.audits["lcp-phases-insight"], .audits["lcp-discovery-insight"]'
```

## 103 Early Hints

[HTTP 103 Early Hints](https://developer.chrome.com/docs/web-platform/early-hints) use "server think time" to send preload directives. While your origin builds HTML, the browser already started fetching CSS and the LCP image.

Benchmarks from [Shopify Engineering](https://www.shopify.com/blog/early-hints) and [Akamai](https://www.akamai.com/blog/performance/improving-performance-with-http-103-early-hints) show **LCP improvements of 300ms to 500ms** at the 75th percentile. [Cloudflare](https://cloudflare.com) and [Vercel](https://vercel.com) support Early Hints at the edge, caching link headers from your origin.

```js
// Cloudflare Workers / Nitro handler
export default defineEventHandler(async (event) => {
  event.node.res.writeEarlyHints({
    link: [
      '</hero.avif>; rel=preload; as=image; fetchpriority=high',
      '</styles.css>; rel=preload; as=style',
    ],
  })

  return await renderPage(event)
})
```

<info>

Best practice from [Shopify's research](https://www.shopify.com/blog/early-hints): limit to **2–4 critical resources** (CSS and LCP image) to avoid bandwidth saturation on mobile.

</info>

## View Transitions and LCP measurement

[View Transitions API](https://developer.chrome.com/docs/web-platform/view-transitions) animates between page states with a compositor snapshot. This changes how Chrome measures LCP on soft navigations.

Same-document View Transitions do not reset LCP. [Cross-document View Transitions](https://developer.chrome.com/docs/web-platform/view-transitions/cross-document), enabled via `@view-transition { navigation: auto }`, count as a new page load and start a fresh LCP measurement.

```css
@view-transition {
  navigation: auto;
}

::view-transition-old(hero),
::view-transition-new(hero) {
  animation-duration: 300ms;
}

.hero-image {
  view-transition-name: hero;
}
```

The LCP element should have a `view-transition-name`. Browsers reuse the snapshot, so the next page reports the old LCP element as already painted. This drops LCP on the destination page to milliseconds.

Watch for regressions. A slow transition animation extends the time before the new LCP element paints. Keep transitions under 400ms.

## Modern image formats

AVIF is table stakes. [Baseline 2024 browser support](https://web.dev/baseline) means no fallback math needed for most audiences. [AVIF delivers 30 to 50% smaller files than WebP](https://web.dev/articles/avif) at the same visual quality.

```html
<picture>
  <source type="image/avif" srcset="/hero.avif">
  <source type="image/webp" srcset="/hero.webp">
  <img src="/hero.jpg" fetchpriority="high" alt="Hero">
</picture>
```

JPEG-XL status: [Safari shipped support in 17](https://caniuse.com/jpegxl), Chrome [remains on hold](https://bugs.chromium.org/p/chromium/issues/detail?id=1178058), Firefox behind a flag. Ship AVIF as primary, JPEG-XL as a progressive enhancement when your CDN supports content negotiation.

Generate responsive AVIF with sharp or a managed service:

```js
import sharp from 'sharp'

await sharp('hero.jpg')
  .resize(1200)
  .avif({ quality: 50, effort: 6 })
  .toFile('hero-1200.avif')
```

For bulk LCP image auditing across a site, scan with [Unlighthouse](/) which reports LCP image formats, dimensions, and byte weight per page.

## Font loading with size-adjust

[`size-adjust`, `ascent-override`, and `descent-override`](https://developer.chrome.com/blog/font-fallbacks) eliminate layout shift from font swaps while keeping text paintable immediately. This makes `font-display: optional` viable for LCP-relevant text.

```css
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: optional;
}

body {
  font-family: 'Inter', 'Inter Fallback', sans-serif;
}
```

`font-display: optional` tells the browser: if the font is not cached within 100ms, use the fallback and never swap. No FOUT, no CLS, and text contributes to LCP on first paint.

For the LCP element specifically, `font-display: swap` with a metrics-matched fallback still wins because the fallback text paints immediately. Use the [Fontaine](https://github.com/nuxt/fonts) or [Capsize](https://seek-oss.github.io/capsize/) library to generate size-adjust values automatically.

## INP impact on paint order

[INP](/learn-lighthouse/core-web-vitals) reshapes paint order. A page with long tasks during hydration pushes LCP later because the compositor waits for the main thread to flush.

Yield to the main thread during hydration:

```js
async function hydrateWithYield(components) {
  for (const component of components) {
    await scheduler.yield()
    hydrate(component)
  }
}
```

[`scheduler.yield()`](https://developer.chrome.com/blog/introducing-scheduler-yield) ships in Chrome 129+. It yields to the browser for paint and input, then resumes. Use it in any loop that runs during hydration.

Break up hydration with `scheduler.postTask()` for priority-aware scheduling:

```js
scheduler.postTask(() => hydrateAboveFold(), { priority: 'user-blocking' })
scheduler.postTask(() => hydrateInteractive(), { priority: 'user-visible' })
scheduler.postTask(() => hydrateBelowFold(), { priority: 'background' })
```

Hydration scheduling alone [can move LCP by 200ms to 500ms](https://web.dev/articles/inp#optimizing_inp) on component-heavy pages.

## HTTP/3 and BBR

[HTTP/3 with QUIC](https://web.dev/articles/performance-http3) eliminates TCP head-of-line blocking. One lost packet no longer stalls every stream. On lossy mobile networks, [HTTP/3 drops TTFB by 10 to 25%](https://blog.cloudflare.com/http3-the-past-present-and-future/).

Every major CDN ships HTTP/3 by default in 2026: Cloudflare, Fastly, Akamai, CloudFront. Verify:

```bash
curl -sI --http3 https://your-site.com | head -1
```

[BBR congestion control](https://blog.cloudflare.com/http3-the-past-present-and-future/) replaces CUBIC on Cloudflare, Google Cloud, and many origins. [BBR probes bandwidth](https://cloud.google.com/blog/products/networking/tcp-bbr-congestion-control-comes-to-gcp-and-your-internet-experience) instead of waiting for packet loss, which improves throughput on high-RTT connections.

Check server support:

```bash
sysctl net.ipv4.tcp_congestion_control
# Should output: bbr or bbr2
```

If your origin runs CUBIC, switch. The change typically shaves 50ms to 200ms off LCP resource load duration.

## Edge rendering and streaming SSR

Edge rendering on Cloudflare Workers, Vercel Edge, or Deno Deploy collapses TTFB to under 100ms globally. With the origin 20ms from the user, the entire LCP budget opens up for rendering and network.

Streaming SSR pairs naturally. Send the critical HTML for the LCP element [before data-dependent sections finish](https://web.dev/articles/streaming-ssr):

```ts
// Nitro / h3 streaming
export default defineEventHandler(async (event) => {
  return new ReadableStream({
    async start(controller) {
      controller.enqueue(encoder.encode(`
        <!DOCTYPE html>
        <html><head>
          <link rel="preload" href="/hero.avif" as="image" fetchpriority="high">
        </head><body>
          <img src="/hero.avif" fetchpriority="high" alt="Hero">
      `))

      const data = await fetchSlowData()
      controller.enqueue(encoder.encode(renderRest(data)))
      controller.enqueue(encoder.encode('</body></html>'))
      controller.close()
    },
  })
})
```

The browser receives the LCP image URL in the first flush. Resource discovery happens while the origin still fetches data for the footer.

## Framework specifics

### Next.js 15 Partial Prerendering

[Partial Prerendering](https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering) prerenders static shells at build time, then streams dynamic holes. The LCP element lives in the static shell, which ships from the edge in under 50ms.

```tsx
// app/page.tsx
export const experimental_ppr = true

export default function Page() {
  return (
    <>
      <Hero />
      <Suspense fallback={<Skeleton />}>
        <PersonalizedContent />
      </Suspense>
    </>
  )
}
```

The `<Hero />` prerenders. The suspended boundary streams from the server. LCP reports on the hero, which is already cached at the edge.

### Nuxt 4 payload extraction

Nuxt 4 automatically extracts `useAsyncData` payloads into static JSON, avoiding a round-trip to the server on client navigation. For SSR pages, enable [payload extraction](https://nuxt.com/docs/guide/concepts/rendering) and preload:

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    payloadExtraction: true,
    renderJsonPayloads: true,
    viewTransition: true,
  },
  nitro: {
    prerender: {
      crawlLinks: true,
      routes: ['/'],
    },
    compressPublicAssets: { brotli: true },
  },
})
```

Pair with `<NuxtImg preload fetchpriority="high">` for the LCP element.

### Astro Islands

[Astro Islands](https://docs.astro.build/en/concepts/islands/) ship zero JavaScript by default. The LCP element is pure HTML, which means [render delay approaches zero](https://docs.astro.build/en/concepts/why-astro/#island-architecture).

```astro
---
import Hero from '../components/Hero.astro'
import InteractiveCart from '../components/Cart.svelte'
---

<Hero />
<InteractiveCart client:visible />
```

`client:visible` hydrates the cart only when it scrolls into view. The LCP hero paints without waiting for any JavaScript bundle.

## Verify with field data

Lab scores lie. [CrUX](https://developer.chrome.com/docs/crux) is what Google ranks on.

Set up Real User Monitoring with the [web-vitals library](https://github.com/GoogleChrome/web-vitals):

```js
import { onLCP } from 'web-vitals/attribution'

onLCP((metric) => {
  navigator.sendBeacon('/api/rum', JSON.stringify({
    value: metric.value,
    element: metric.attribution.element,
    url: metric.attribution.url,
    timeToFirstByte: metric.attribution.timeToFirstByte,
    resourceLoadDelay: metric.attribution.resourceLoadDelay,
    resourceLoadDuration: metric.attribution.resourceLoadDuration,
    elementRenderDelay: metric.attribution.elementRenderDelay,
  }))
}, { reportAllChanges: true })
```

The [attribution build](https://github.com/GoogleChrome/web-vitals#attribution) reports the four LCP sub-parts per user. Aggregate at the 75th percentile to match CrUX.

## Scan every page

Optimizing the homepage while blog posts fail is the most common pattern. Different templates have different LCP elements, different preload strategies, and different render paths.

[Unlighthouse](/) scans every page on your site and reports LCP per template. [Bulk PageSpeed](/tools/bulk-pagespeed) compares lab and field data at scale. [LCP Finder](/tools/lcp-finder) identifies the exact LCP element on any URL.

Start with the `/learn-lighthouse/lcp` hub for issue-specific fixes, then layer in the 2026 optimizations above.

## 2026 LCP Optimization Strategy

<table>
<thead>
  <tr>
    <th>
      Phase
    </th>
    
    <th>
      Action
    </th>
    
    <th>
      Tool
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Discovery
      </strong>
    </td>
    
    <td>
      <code>
        fetchpriority="high"
      </code>
      
       + 103 Early Hints
    </td>
    
    <td>
      <a href="/tools/lcp-finder">
        LCP Finder
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Network
      </strong>
    </td>
    
    <td>
      AVIF + HTTP/3 + BBR
    </td>
    
    <td>
      <a href="/">
        Unlighthouse
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Render
      </strong>
    </td>
    
    <td>
      <code>
        scheduler.yield
      </code>
      
       + Critical CSS
    </td>
    
    <td>
      Lighthouse 13
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Next Page
      </strong>
    </td>
    
    <td>
      Speculation Rules (Moderate)
    </td>
    
    <td>
      CrUX
    </td>
  </tr>
</tbody>
</table>

<u-button :trailing="true" icon="i-heroicons-rocket-launch" label="Scan Your Site with Unlighthouse" size="lg" to="/">



</u-button>
</lighthouse-devtools-mockup>
</interactive-lcp-timeline>
</audit-impact>
