Skip to main content

How to Cut LCP With an Image Pipeline Without Touching the Theme

5 min read
Improve my LCP score

We fix the format, size and priority of your LCP image without changing the theme.

On most content and commerce pages the Largest Contentful Paint element is a single image, which means one image decides the score. This guide covers the decisions that belong to the delivery layer rather than to a template, from format negotiation on the Accept header to the size actually credited by the metric, the priority the browser assigns to an image before layout, and the one attribute that quietly ruins a hero. Every attribute here is checked against the HTML standard or web.dev, including which images must never be lazy loaded.

On most content and commerce pages the Largest Contentful Paint element is one image. Fix how that image is chosen, sized, prioritised and delivered and the score moves, whatever framework rendered the page around it. Almost none of those decisions have to live in a template.

Know which element you are actually optimising

The metric only considers a short list of elements, which was kept deliberately small. Images, an image inside an svg, video elements, an element with a background image loaded through url(), and block level elements containing text. Good is 2.5 seconds or less, judged at the 75th percentile of page loads, with mobile and desktop measured separately.

One detail in that definition pays for itself immediately. For an image resized from its natural dimensions, the size credited is the visible size or the intrinsic size, whichever is smaller. Shipping a 3000 pixel wide hero into an 800 pixel slot buys you nothing in the metric and costs your visitor the difference in bytes.

Split the number before you tune it

web.dev breaks LCP into four parts with rough targets. Time to first byte at around 40 percent, resource load delay under 10 percent, resource load duration at around 40 percent, and element render delay under 10 percent.

Measure the split before you touch anything. A slow origin and a bloated hero image are both common, they need opposite fixes, and the four part breakdown tells you which one you have in about a minute.

Negotiate the format at the edge

Browsers already tell you what they can decode. A current browser requesting an image sends an Accept header along the lines of image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5, so the server can pick the format without any markup change at all.

# /etc/nginx/conf.d/avif-negotiation.conf
map $http_accept $avif_suffix {
    default        "";
    "~*image/avif" ".avif";
}
# inside the server block that serves your media
location ~* ^/media/.+\.(jpe?g|png)$ {
    add_header Vary Accept;
    try_files $uri$avif_suffix $uri =404;
}

You pre-generate hero.jpg.avif next to hero.jpg, and a browser that advertises AVIF gets it. When the map yields an empty string the first candidate is just the original path, so the fallback needs no extra branch.

Two things make or break this. Check that your nginx ships the AVIF type with grep avif /etc/nginx/mime.types, because without it the file goes out with the wrong content type. And keep the Vary header, since it is what tells every cache between you and the visitor that this URL has more than one correct answer. The same Vary value has to appear on every response for that URL, including 304 responses.

Give priority to exactly one image

Images start life at low priority and are raised to high at layout time if they turn out to be in the viewport. That promotion is real work that happens after layout, and it is time your hero spends waiting.

<img src="/media/hero.jpg" width="1200" height="675" fetchpriority="high" alt="">

The fetchpriority attribute takes high, low or auto, and high on the LCP image is the documented use. The discipline is the point. Mark one image high per page, and consider low for the carousel slides that are not on screen yet, so the browser spends its first connections on the one that counts.

If the image is already preloaded as one of the first things in the head, adding high priority to the tag will not change much on top of it. Preloaded images should carry fetchpriority="high" on the preload itself, otherwise the preload inherits the default priority for its as value.

The attribute that quietly ruins a hero

Lazy loading is the single most common self-inflicted LCP wound, usually installed by a plugin that applies it to every image on the page. The guidance from web.dev is direct. Do not lazy load images likely to be in the viewport at load, especially the LCP image, and use loading="lazy" only for images outside the initial viewport.

Two supporting rules travel with it. Always set width and height, since an image with no dimensions is laid out as zero by zero, which both invites layout shift and can convince the browser that an entire gallery fits on screen. And for images you do lazy load, sizes="auto" lets the browser use the element's own laid out width to pick from srcset, which saves maintaining the same breakpoints twice. It is valid only alongside loading="lazy", and where it cannot be resolved the browser falls back through the rest of the sizes list.

When the hero lives in CSS

A background image set in a stylesheet is invisible to the preload scanner, so the browser cannot start it until the CSS has been fetched and applied. That is pure load delay, the subpart that should be under 10 percent. Preload it from the head instead.

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

Preload the format you most want served rather than one link per format, and let browsers without support fall through to the normal path.

Prove it from the command line

# does the edge actually negotiate, and does it say so to caches
curl -sI -H 'Accept: image/avif,image/webp,image/*;q=0.8' \
  https://example.com/media/hero.jpg | grep -i 'content-type\|vary\|cache-control'

# and the same URL for a client that cannot take AVIF
curl -sI -H 'Accept: image/png,image/*;q=0.8' \
  https://example.com/media/hero.jpg | grep -i 'content-type'

Two responses, two content types, one Vary: Accept on both. If the second request also returns AVIF, something upstream is ignoring the negotiation and a share of your visitors are getting an image they cannot render.

Most of this survives a theme upgrade precisely because it never entered the theme. If you want the delivery layer built and measured rather than guessed at, that is what our server setup and optimization work does, and monitoring and observability is how you keep the number honest once it drops. For the field data side of the same problem, see measuring real-world performance beyond Lighthouse.

Frequently asked questions

What is a good LCP score and which elements can be the LCP element?

A Largest Contentful Paint of 2.5 seconds or less is good, judged at the 75th percentile of page loads, with mobile and desktop measured separately. Only a short list of elements can be the LCP element, namely images, an image inside an svg, video elements, an element with a background image loaded through url(), and block level elements containing text. On most content and commerce pages that element is a single image.

Does serving a hero image larger than its display size help LCP?

No. For an image resized from its natural dimensions, LCP credits the visible size or the intrinsic size, whichever is smaller. A 3000 pixel wide hero shipped into an 800 pixel slot buys nothing in the metric and costs the visitor the difference in bytes.

How do I find out which part of LCP is slow?

Break Largest Contentful Paint into its four parts before tuning anything. web.dev gives rough targets of about 40 percent for time to first byte, under 10 percent for resource load delay, about 40 percent for resource load duration and under 10 percent for element render delay. A slow origin and a bloated hero image are both common and need opposite fixes, and the breakdown shows which one a page has in about a minute.

How do I serve AVIF images with nginx without changing the HTML?

Pre-generate an AVIF copy beside each original, such as hero.jpg.avif next to hero.jpg, map $http_accept so a request advertising image/avif gets an .avif suffix, and use try_files $uri$avif_suffix $uri =404 in the media location. Add a Vary Accept header to every response for that URL, 304 responses included, so caches know the URL has more than one correct answer. Run grep avif /etc/nginx/mime.types as well, since without the AVIF type the file goes out with the wrong content type.

Should the LCP image use fetchpriority high?

Yes, fetchpriority set to high on the LCP image is the attribute's documented use, and the attribute also accepts low and auto. Images start at low priority and are raised to high only at layout time if they turn out to be in the viewport, which leaves the hero waiting. Mark exactly one image high per page and consider low for carousel slides not yet on screen. For a preloaded image, put fetchpriority high on the preload link itself, or the preload inherits the default priority for its as value.

Can lazy loading make LCP worse?

Lazy loading is the most common self-inflicted LCP wound, usually added by a plugin that applies it to every image on the page. web.dev advises against lazy loading images likely to be in the viewport at load, especially the LCP image, and recommends the lazy loading attribute only for images outside the initial viewport. Always set width and height too, because an image with no dimensions is laid out as zero by zero, which invites layout shift and can convince the browser that a whole gallery fits on screen.

How do I preload a CSS background hero image to improve LCP?

A background image set in a stylesheet is invisible to the preload scanner, so the browser cannot start it until the CSS has been fetched and applied, and that wait is pure resource load delay. Preload it from the head with a link rel=preload as=image, using imagesrcset and imagesizes for the responsive candidates and fetchpriority high. Preload only the format most wanted, such as AVIF, and let browsers without support fall through to the normal path.

How do I test AVIF content negotiation with curl?

Request the same image URL twice with curl -sI, once with an Accept header that includes image/avif and once with an Accept header that lists image/png and no AVIF. The first response should report an AVIF content type and the second the original type, and both should carry a Vary header of Accept. If the second request also returns AVIF, something upstream is ignoring the negotiation and some visitors are getting an image they cannot render.

Read how we handle it in Cloud & Server Management.