Web Performance · 9 min read

Mastering LCP: Subparts, Priority Hints, Critical CSS and Images

Break Largest Contentful Paint into its four subparts and apply the fix that matches the slow one, from priority hints to critical CSS and image delivery.

By Praful Patel

Short answer

Split LCP into its four subparts before you optimise anything, because each subpart has a different fix. Resource load delay is fixed by making the LCP image discoverable in the HTML with fetchpriority="high". Load duration is fixed by image size, format and CDN. Element render delay is fixed by removing render-blocking CSS and JavaScript, which is where critical CSS inlining helps. Compressing the hero image does nothing if render delay is the bottleneck.

Most LCP advice is a list: compress images, preload the hero, inline critical CSS, use a CDN. All of it is sometimes right, and applied blindly much of it does nothing. Google’s own guidance shows a case where shrinking the image simply moved the waiting time into render delay and LCP did not change.

This article is about the browser side of LCP: the subparts, priority hints, critical CSS and image delivery. Server response time is the first subpart and often the largest, and it is covered in my full-stack performance guide.

The four LCP subparts

LCP is good at 2.5 seconds or less and poor above 4 seconds, measured at the 75th percentile of page visits. web.dev’s LCP optimisation guide breaks it into four subparts that add up exactly to the total:

Subpart From → to Rough target share Usual fix
Time to First Byte Navigation start → first byte of HTML ~40% Caching, fewer redirects, less server work
Resource load delay TTFB → LCP resource starts downloading <10% Discoverable in HTML, fetchpriority, preload
Resource load duration Download start → download end ~40% Smaller files, modern formats, CDN
Element render delay Download end → element painted <10% Remove render-blocking CSS/JS, avoid client-side rendering

The shares are guidelines from the same guide, not fixed millisecond targets. If the LCP element is text, both resource subparts are zero and the time is split between TTFB and render delay.

Measure the breakdown first

In Chrome DevTools, record a page load in the Performance panel. The Insights sidebar shows an LCP breakdown insight with the four subparts, an LCP request discovery insight that flags lazy-loading, late discovery and missing fetchpriority, and Render-blocking requests.

In the field, the web-vitals attribution build reports the same subparts per page view, which tells you which one dominates for real users on real networks:

// Assumes web-vitals v5 and a /rum endpoint.
import { onLCP } from 'web-vitals/attribution';

onLCP(({ value, attribution }) => {
  navigator.sendBeacon('/rum', JSON.stringify({
    metric: 'LCP',
    value,
    element: attribution.target,
    ttfb: attribution.timeToFirstByte,
    loadDelay: attribution.resourceLoadDelay,
    loadDuration: attribution.resourceLoadDuration,
    renderDelay: attribution.elementRenderDelay,
  }));
});

Resource load delay: priority hints and discovery

The browser cannot download an image it has not found. An image in an <img> tag in the server HTML is found by the preload scanner almost immediately. An image set as a CSS background, injected by JavaScript, or behind data-src for a lazy-loading library is found much later.

Being found is not enough, either. Per web.dev’s Fetch Priority guide, images start at low priority; since Chrome 117 the first five large images start at medium, and images in the viewport are only boosted to high once layout has happened. fetchpriority="high" tells the browser up front:

<!-- The LCP image: in the HTML, eager, high priority, sized. -->
<img
  src="/images/hero-1200.avif"
  srcset="/images/hero-800.avif 800w, /images/hero-1200.avif 1200w, /images/hero-1600.avif 1600w"
  sizes="(max-width: 800px) 100vw, 1200px"
  width="1200" height="630"
  alt="Dashboard showing the quarterly report"
  fetchpriority="high">

<!-- Off-screen carousel slides: explicitly low. -->
<img src="/images/slide-2.avif" width="1200" height="630" alt="…" fetchpriority="low" loading="lazy">

Rules that follow:

  • Never lazy-load the LCP image. A blanket loading="lazy" on every image is a common self-inflicted LCP regression.
  • Use high on one or two resources at most. Marking everything high returns you to having no priorities.
  • Preload only what the HTML cannot reveal. For a CSS background image, add <link rel="preload" as="image" href="…" fetchpriority="high">; a preload on its own still loads at the image’s default priority. For a normal <img> in the HTML, the attribute is enough.
  • Serve the LCP image from the same origin as the HTML where you can, or preconnect to the image CDN, to avoid a new connection on the critical path.

fetchpriority is a hint, and the browser may override it. It has been Baseline since late 2024 (Chrome and Edge 101, Safari 17.2, Firefox 132), so it is safe to ship without a fallback. web.dev reports an example where Google Flights’ LCP improved from 2.6 s to 1.9 s with the attribute on its hero image; your result depends on how much bandwidth contention the page has.

In Next.js

Since Next.js 16 the priority prop on next/image is deprecated in favour of preload, and the Image component docs now recommend loading="eager" or fetchPriority="high" in most cases instead. Use preload only when you need the request to start from the <head>, and not when different images are the LCP element at different viewport sizes.

import Image from 'next/image';

export function Hero() {
  return (
    <Image
      src="/images/hero.jpg"
      alt="Dashboard showing the quarterly report"
      width={1200}
      height={630}
      sizes="(max-width: 800px) 100vw, 1200px"
      fetchPriority="high"
      loading="eager"
    />
  );
}

Resource load duration: image delivery

Once the download starts at the right time, its length is about bytes and distance:

  • Size to the layout, not to the source file. srcset and an accurate sizes let a phone download an 800px file instead of a 2400px one. An incorrect sizes (or none, with 100vw implied) is a frequent reason a responsive image is still too big.
  • Use AVIF or WebP. Use <picture> with a JPEG fallback if you serve the format statically, or an image CDN that negotiates formats from the Accept header.
  • Cache aggressively. Fingerprinted image URLs can carry a long Cache-Control lifetime, so repeat views skip the network.
  • Do not compete with yourself. Several high-priority fonts, scripts and images downloading at once share the same bandwidth. Lowering everything that is not needed for the first view speeds up the one that is.

If the LCP element is text, the equivalent work is on the font: use a font-display value other than block or auto so text paints in a fallback font without waiting. Choosing a fallback that does not shift the layout is covered in eliminating CLS in server-rendered applications.

Element render delay: critical CSS and blocking scripts

The image can be fully downloaded and still not painted. The usual causes are stylesheets that have not arrived yet, synchronous scripts in the <head>, long tasks on the main thread, and content that only appears once client-side JavaScript runs.

Inline critical CSS, but only a little

External stylesheets block rendering. Inlining the CSS needed for the first viewport lets the browser paint without waiting for that request. web.dev’s critical CSS guide suggests keeping above-the-fold content under about 14 KB compressed, the amount that typically fits in the first round trip. Tools such as Critical and Penthouse extract the rules automatically.

<head>
  <style>
    /* Critical rules only: layout shell, header, hero. Generated at build time. */
    .site-header{display:flex;align-items:center;height:64px}
    .hero{display:grid;gap:1.5rem;padding:2rem 1rem}
    .hero img{width:100%;height:auto}
  </style>
  <link rel="preload" href="/css/site.css" as="style">
  <link rel="stylesheet" href="/css/site.css" media="print" onload="this.media='all'">
  <noscript><link rel="stylesheet" href="/css/site.css"></noscript>
</head>

The trade-off is caching: inlined CSS is downloaded again with every HTML response, while an external stylesheet is cached across pages. Inline the minimum, and only if the render-blocking CSS request is actually on the critical path in your trace. Next.js offers an experimental.inlineCss flag that inlines all CSS rather than only the critical subset. Its documentation recommends it for small atomic CSS such as Tailwind and against it for large CSS bundles or sites with many returning visitors.

Scripts and client rendering

  • No synchronous third-party scripts in the head. Use defer or async. A/B testing snippets that hide the page until they run are a direct addition to render delay.
  • Render the LCP element on the server. If the hero only appears after a client-side fetch, its render delay includes JavaScript download, execution and an API round trip. Server rendering or prerendering removes that. With streaming, keep the LCP element in the initial shell rather than inside a slow Suspense boundary. Streaming architecture in Next.js covers where to put those boundaries.
  • Break up long tasks during load. Images are painted on the main thread, so a long hydration task can delay the paint after the download finishes. The same yielding techniques used to improve INP apply here too.

LCP checklist by subpart

  • Measure: collect LCP attribution in the field; read the LCP breakdown insight in DevTools.
  • TTFB: cacheable HTML at the edge, no redirect chains, profiled server work. Next.js cache layers are explained in Next.js caching explained.
  • Load delay: LCP image in the server HTML as an <img>, not lazy-loaded, fetchpriority="high"; preload only CSS background images.
  • Load duration: correct srcset/sizes, AVIF or WebP, a CDN, long cache lifetimes, no competing high-priority downloads.
  • Render delay: small inlined critical CSS, no synchronous head scripts, no anti-flicker snippets, LCP element server-rendered.

Questions people actually ask

Should I use preload or fetchpriority=”high” for the LCP image?
For an <img> already in the server HTML, fetchpriority="high" is enough, because the browser finds it early anyway. Preload is for images the HTML does not reveal, such as CSS backgrounds, and should carry fetchpriority="high" itself.

Does inlining critical CSS always improve LCP?
No. It helps only when a render-blocking stylesheet is delaying the first paint. If your LCP problem is TTFB or a late-discovered image, inlining adds HTML weight and removes stylesheet caching without fixing the bottleneck.

Why is my LCP image not getting high priority even with preload?
A preload loads at the resource type’s default priority, which for images is low or medium until layout. Add fetchpriority="high" to the preload link, and check the Priority column in the DevTools Network panel to confirm.

LCP stuck above 2.5 seconds?

I break LCP down by subpart on your real templates and fix the one that is actually slow, from server response to image delivery and render-blocking resources.

Request an LCP review →
← Back to all insights