Choosing LCP Image Formats, Sizes and srcset
The hero image is the Largest Contentful Paint element on most content and commerce pages, so its format, its dimensions and the srcset that selects it decide the metric. This guide sits under LCP Measurement & Optimization and covers the part teams usually get half-right: they convert to AVIF, ship one enormous source, and are surprised when the p75 on mid-tier Android barely moves. Transfer size is only one of the two costs. Decode is the other, and it is paid entirely on the main thread of the weakest device in your traffic.
Prerequisites
- A hero image you have confirmed is the LCP element in the field, not just in a lab run — the attribution data from Debugging Web Vitals with the Attribution Build tells you this.
- An image pipeline (build step, CDN transform or media service) that can emit multiple formats and widths from one source.
- Field data segmented by device tier, so you can see the decode-bound cohort separately, as set out in Device & Network Segmentation.
- The rendered box dimensions of the hero at each breakpoint. Not the design mock — the computed size in the browser.
How to choose the format and the widths
Step 1 — Measure the rendered box, then size for it
The single largest waste in most hero images is serving a 2400 px-wide file into a 390 px-wide box. Browsers scale it down for free visually, and charge you for every byte and every decoded pixel.
// Run in the console on a real breakpoint to get the numbers that matter.
const img = document.querySelector('img[fetchpriority="high"], .hero img');
const box = img.getBoundingClientRect();
console.table({
cssWidth: Math.round(box.width),
dpr: window.devicePixelRatio,
neededWidth: Math.round(box.width * window.devicePixelRatio),
intrinsicWidth: img.naturalWidth,
wastedFactor: +(img.naturalWidth / (box.width * window.devicePixelRatio)).toFixed(2),
});
A wastedFactor above about 1.2 means you are paying for pixels that are thrown away. Cap the largest candidate at roughly 2× the largest CSS box you actually render; going beyond 2× DPR is imperceptible on every shipping display and costs real milliseconds on decode.
Step 2 — Choose formats by decode cost, not only by size
AVIF is usually the smallest and is consistently the slowest to decode. WebP is a middle point. On a fast desktop the difference is noise; on the mid-tier cohort where your LCP is actually failing, it is measurable.
| Format | Relative transfer size | Relative decode time (mid-tier Android) | Use when |
|---|---|---|---|
| JPEG (mozjpeg, q80) | 1.00 | 1.00 | Universal fallback; still fine for small heroes |
| WebP (q78) | 0.68 | 1.15 | Default choice for most heroes |
| AVIF (q60, speed 6) | 0.48 | 2.30 | Large heroes on bandwidth-bound cohorts |
The rule that follows from the table: AVIF wins when the file is big enough that the transfer saving exceeds the extra decode. For a 40 KB hero it usually loses; for a 260 KB one it usually wins. Measure both against your own traffic rather than adopting the ordering as doctrine.
Step 3 — Write a srcset the browser can actually choose from
Width descriptors plus an accurate sizes attribute let the browser pick before layout. This is the part that is most often wrong: sizes="100vw" on an image that renders in a 640 px column makes every phone download the widest candidate.
<picture>
<source
type="image/avif"
srcset="/hero-400.avif 400w, /hero-800.avif 800w, /hero-1200.avif 1200w, /hero-1600.avif 1600w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 66vw, 640px">
<source
type="image/webp"
srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w, /hero-1600.webp 1600w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 66vw, 640px">
<img
src="/hero-800.jpg"
srcset="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 66vw, 640px"
width="1600" height="900"
alt="Product hero"
fetchpriority="high"
decoding="async">
</picture>
Four things in that markup are load-bearing. The sizes values match the real layout at each breakpoint. width and height are present so the box is reserved and no layout shift is contributed — the reasoning is in CLS Reduction Strategies. fetchpriority="high" moves the request up the queue, as covered in Optimizing LCP with fetchpriority & preload. And decoding="async" keeps the decode off the critical rendering path for everything except the LCP element itself.
Step 4 — Keep the candidate ladder short
Every extra width in a srcset is another cache entry at the CDN and another variant to invalidate. Four widths covering 400–1600 px serve every realistic device; twelve widths mostly serve your CDN bill.
Step 5 — Verify the browser chose what you expected
// Report the actually-selected candidate alongside the LCP beacon.
new PerformanceObserver((list) => {
const entry = list.getEntries().at(-1);
if (!entry?.element || entry.element.tagName !== 'IMG') return;
const img = entry.element;
navigator.sendBeacon('/rum', JSON.stringify({
metric: 'LCP',
value: Math.round(entry.startTime),
// currentSrc is the candidate the browser actually picked.
chosen: img.currentSrc,
renderedWidth: Math.round(img.getBoundingClientRect().width),
intrinsicWidth: img.naturalWidth,
dpr: window.devicePixelRatio,
}));
}).observe({ type: 'largest-contentful-paint', buffered: true });
Verifying it works
Three checks, in order of how often they catch something:
- Chosen-candidate distribution. Group beacons by
chosenand viewport. If phones are picking the 1600 px candidate, thesizesattribute is wrong, not the pipeline. - Waste ratio in the field.
intrinsicWidth / (renderedWidth * dpr)above 1.5 at p75 means the ladder is mis-sized for real devices. - p75 LCP by device tier before and after. The bandwidth-bound cohort should move on transfer size; the CPU-bound one should move on pixel count. If neither moves, the hero was not the LCP element.
When the fallback matters more than the winner
Every browser that cannot decode your first-choice format falls through to the next <source>, and on a <picture> element that fallback is chosen without any extra round trip. This is why the ladder above lists AVIF, then WebP, then a JPEG in the <img> itself: the cost of carrying three formats is build time and storage, not latency.
What does cost latency is a fallback that is dramatically heavier than the winner. If your AVIF hero is 44 KB and your JPEG fallback is 310 KB, then every session on a browser without AVIF support pays seven times the transfer. Generate the fallback at the same rendered dimensions and with a sensible quality setting rather than shipping whatever the original asset happened to be — the fallback is a real file served to real users, not a formality.
The same reasoning applies to quality settings. A quality level tuned by eye on a desktop display is usually 10–15 points higher than it needs to be for a hero that renders at 390 px wide on a phone. Dropping a WebP hero from q85 to q78 is typically a 20% transfer saving that nobody can see, and it applies to every request rather than only to the browsers that support the newest format.
Edge cases and gotchas
sizesis evaluated before CSS is applied. The browser uses the attribute, not your stylesheet, to pick a candidate during preload scanning. If they disagree, the attribute wins and the layout looks fine while the wrong file downloads.decoding="async"on the LCP element itself. It is the right default for everything else and can slightly delay the LCP paint for the hero. Test both on your own traffic; the difference is small and not always in the same direction.- Background images cannot use
srcset. A CSSbackground-imagehero has no responsive selection short ofimage-set()and media queries, and it is invisible to the preload scanner. Move the hero to an<img>if you can. - AVIF encoding speed settings change the decode profile. A file encoded at speed 0 is smaller and no slower to decode; one encoded at speed 9 is larger. Encoder settings belong in the pipeline, not in a per-image decision.
- Art direction is not the same as resolution switching. If mobile and desktop crop differently you need multiple
<source>elements with media conditions, and each one needs its own correctsizes. - CDN auto-format can defeat your ladder. A CDN that transcodes on
Acceptheaders may serve AVIF to a client whose decode cost you measured for WebP. Know which layer is choosing. - Cache fragmentation. Formats × widths × DPR multiply into a lot of cache keys, and a low hit ratio at the edge costs more TTFB than the format saved in transfer — see Reducing TTFB with Edge Caching.
FAQ
Is AVIF always the right choice for a hero image?
No. It is the smallest and the slowest to decode. On large heroes over bandwidth-constrained connections it wins comfortably; on small heroes served to CPU-bound devices the extra decode can cancel the transfer saving. Measure both on the cohort that is currently failing.
How many widths should a srcset contain?
Four is almost always enough — roughly 400, 800, 1200 and 1600 px. More candidates fragment your CDN cache without changing what the browser picks.
Why is the browser downloading the largest image on a phone?
Because sizes claims the image occupies more of the viewport than it does. The preload scanner uses the attribute before any CSS is applied, so a wrong sizes overrides a correct stylesheet every time.
Do width and height attributes still matter if I use aspect-ratio in CSS?
Yes. The attributes are available to the preload scanner and to the layout engine before your stylesheet loads, which is exactly the window in which a shift would otherwise happen.
Related
- LCP Measurement & Optimization — the metric this guide is optimising, and how to capture it correctly.
- Optimizing LCP with fetchpriority & preload — getting the chosen file requested earlier.
- Device & Network Segmentation — separating the decode-bound cohort from the bandwidth-bound one.