Image Optimization: Formats, srcset and Priority
Pick the format, let srcset and sizes choose the pixels for each screen, and load the hero first without layout shift: the three decisions behind fast images.
Images are usually the heaviest bytes on a page and most often the element that decides LCP (Largest Contentful Paint). Making them fast comes down to three decisions per image: the format (AVIF or WebP instead of JPEG or PNG), the pixel size for this particular screen (declared with srcset and sizes, so the browser picks the smallest file that still looks sharp), and the priority (the hero loads first and eagerly, everything below the fold loads lazily, and every image reserves its space so nothing shifts).
Context
For most of the web's history an <img> pointed at one file and every device downloaded it. Two things broke that. In 2010 the iPhone 4 shipped a screen with a DPR (device pixel ratio) of 2, four physical pixels for every CSS pixel, so images had to double in width to look sharp. And phones on slow networks started downloading desktop-sized photos. The Responsive Images Community Group answered with srcset, sizes and <picture>, which shipped in Chrome 38 in 2014 and in every major browser soon after. Formats moved on too: Google released WebP in 2010, and AVIF (AV1 Image File Format, derived from the AV1 video codec) became Baseline across all major browsers in January 2024. Native loading="lazy" arrived in Chrome 76 in 2019.
You have met this as Lighthouse's "Properly size images" and "Serve images in next-gen formats" audits, as the w=800&q=75 parameters in a Cloudinary, imgix or Vercel image URL, and as the next/image component. The problem in its simplest form: a photo straight from a phone camera, displayed small.
<!-- 4032 x 3024 JPEG from a phone, about 3.5 MB, shown 360 px wide -->
<img src="/team.jpg" alt="The team at the offsite">
<!-- the same picture at the width the screen needs, as AVIF: ~40 KB -->
<img src="/team-720.avif" width="720" height="540"
alt="The team at the offsite">- Intrinsic vs rendered size
- Intrinsic is the file’s own pixel dimensions; rendered is how many CSS pixels it occupies on the page. The gap between them is wasted bytes or blur.
- DPR (device pixel ratio)
- Physical pixels per CSS pixel: 1 on most desktop monitors, 2 to 3 on phones and high-density laptops.
- w descriptor
- In srcset, 800w means "this file is 800 pixels wide". The browser combines it with sizes to choose a file.
- sizes
- Tells the browser how wide the image will render, before layout has run, e.g. 100vw on phones and 50vw on desktop.
- Art direction
- Serving a different crop or composition at different screen sizes, not just a smaller copy. That is what <picture> with media queries is for.
- LQIP (low-quality image placeholder)
- A tiny blurred version or a solid dominant colour shown while the real image loads.
Why it matters
HTTP Archive's Web Almanac consistently finds images to be the largest share of page weight and the LCP element on most mobile pages. That makes them the cheapest big win in web performance: no refactor, just better files and attributes. They also cause a large share of CLS (Cumulative Layout Shift), because an image without reserved space pushes content down when it arrives. And on metered mobile data, every unnecessary megabyte costs the user money.
Choose the format
Modern codecs compress photos far better than JPEG at the same visual quality. AVIF typically produces files around half the size of an equivalent JPEG and noticeably smaller than WebP, at the cost of slower encoding. Because every current browser supports both, the practical default for photos is AVIF with a WebP or JPEG fallback, and SVG (Scalable Vector Graphics) for logos, icons and illustrations.
| Format | Best for | Support | Watch out for |
|---|---|---|---|
| JPEG | Photos, universal fallback | Everywhere | Largest of the photo formats; no transparency |
| PNG | Screenshots, UI with sharp edges, transparency | Everywhere | Huge for photos; lossless only |
| WebP | Photos and transparency, smaller than JPEG | All major browsers since Safari 14 (2020) | Gains over a well-tuned JPEG are modest |
| AVIF | Photos, best compression today | Baseline since January 2024 | Slow to encode; generate at build time or in a CDN |
| SVG | Logos, icons, illustrations | Everywhere | Not for photos; sanitize user-uploaded SVG (it can carry script) |
<picture>
<!-- the browser takes the first source whose type it supports -->
<source type="image/avif" srcset="/hero-1600.avif">
<source type="image/webp" srcset="/hero-1600.webp">
<img src="/hero-1600.jpg" width="1600" height="900"
alt="Dashboard showing this week's study plan">
</picture>Serve the right pixels: srcset and sizes
The browser starts fetching images before it has laid out the page, so it cannot measure the slot an image will fill. sizes tells it the rendered width in advance; srcset lists the files available. The browser multiplies the slot width by the screen's DPR and takes the smallest candidate at least that wide. It may pick a smaller one when the user has data saving on, or a larger one it already has in cache.
<img
src="/hero-1280.jpg"
srcset="/hero-640.avif 640w, /hero-960.avif 960w,
/hero-1280.avif 1280w, /hero-1600.avif 1600w,
/hero-2400.avif 2400w"
sizes="(max-width: 768px) 100vw, 50vw"
width="1600" height="900"
alt="Dashboard showing this week's study plan"
fetchpriority="high" decoding="async">Fixed-size images and art direction
An avatar that is always 48 CSS pixels does not need sizes: density descriptors say which file suits which DPR, as in srcset="/a-48.webp 1x, /a-96.webp 2x". When the picture itself should change, for example a wide banner on desktop and a tight square crop on a phone, use <picture> with media on each <source>. Unlike srcset, which is a hint the browser may override, a matching media source is a command.
- 1A blog card renders 360 CSS pixels wide on a phone with DPR 2, so it needs 720 physical pixels.
- 2The uploaded original is 4032 × 3024, about 3.5 MB as JPEG: over five times wider than needed, roughly 30 times the pixels.
- 3Resized to 720 pixels wide and encoded as AVIF at quality 55, the same image is a few tens of kilobytes and looks identical at that size.
- 4A 3× phone would ask for 1080 pixels. For photos many teams cap at 2×: the extra sharpness is hard to see and the file grows by about 2.25 times.
- 5Multiply by twelve cards in a grid and the page drops from tens of megabytes to well under one.
Load order and layout stability
Bytes are only half of it; the other half is when each image starts downloading. The LCP image should be in the initial HTML, eager, and marked high priority, which has been supported in all major browsers since Firefox added fetchpriority in late 2024. Images below the fold should be lazy so they do not compete for bandwidth. Every image needs its dimensions declared so the browser reserves the box before the bytes arrive.
/* fluid image that still reserves its box before loading */
.card img {
width: 100%;
height: auto; /* keeps the ratio from width/height attrs */
aspect-ratio: 4 / 3; /* or set it here when attrs are unknown */
object-fit: cover; /* crop instead of stretching */
background: #e9e4dc; /* dominant-colour placeholder */
}Image CDNs and next/image
Producing five widths in two formats for every image by hand does not scale. An image CDN (content delivery network) resizes and encodes on the first request, then caches the result at the edge. It reads the request's Accept header to decide between AVIF, WebP and JPEG, so the same URL can return different bytes, which is why the response must carry Vary: Accept or include the format in the cache key.
next/image is this pattern plus the right markup. It generates the srcset from its deviceSizes and imageSizes config, requires width and height (or fill) so space is reserved, lazy-loads by default and decodes asynchronously. Two defaults are worth changing. It serves only WebP unless you add AVIF to images.formats, and since Next.js 16 the old priority prop is deprecated: mark the hero with fetchPriority="high" or loading="eager", and use preload only when it must start from the head.
import Image from 'next/image'
import hero from './hero.jpg' // static import: width, height, blur
export function Hero() {
return (
<Image
src={hero}
alt="Dashboard showing this week's study plan"
sizes="(max-width: 768px) 100vw, 50vw"
fetchPriority="high" // LCP image: not lazy
placeholder="blur" // LQIP from the static import
/>
)
}
// next.config.ts
// images: { formats: ['image/avif', 'image/webp'] }Pitfalls
- w descriptors without sizes
Without
sizes, the browser assumes the image fills the viewport (100vw). A thumbnail in a four-column grid on a 2× desktop then downloads a 3000-pixel file. Always state the real rendered width, at least roughly, for each breakpoint. - Lazy or late-discovered LCP image
A hero that is lazy, inserted by JavaScript, or set as a CSS background is found late by the browser, so its download starts after other work. Put it in the server-rendered HTML as an
<img>with high priority, or preload it when it has to stay a background. - No dimensions
An image without
width/heightoraspect-ratiois zero pixels tall until it loads, then pushes everything below it down. That is a layout shift on every page view with a slow image, and it also makes users click the wrong thing. - Caching transformed images without Vary
If a CDN caches the AVIF response for a URL without
Vary: Acceptor a format-aware cache key, the next browser that cannot decode AVIF receives it anyway and shows a broken image. Format negotiation and caching must be configured together. - PNG for photos and huge inline placeholders
A photo saved as PNG is often several times larger than the JPEG, never mind AVIF. At the other extreme, a large base64 placeholder inlined into the HTML delays the first byte of everything else; keep LQIPs to a few hundred bytes or use a solid colour.
Interview questions
Q1How does the browser choose an image from srcset?
It evaluates sizes against the viewport to get the slot width in CSS pixels, multiplies by the device pixel ratio, and picks the smallest candidate whose w descriptor covers that. With density descriptors it matches 1x or 2x directly. The choice is a hint, so the browser may take a smaller file on data saver or reuse a larger one already in cache.
Q2Walk me through optimizing the hero image of a landing page.
First make sure it is an <img> in the server-rendered HTML, not a CSS background or JS-inserted. Then generate AVIF and WebP at several widths, serve them through srcset with an accurate sizes, set fetchpriority="high", never lazy-load it, and declare width and height. Finally measure LCP in the field and check which sub-part, load delay or load time, actually dominates.
Q3What happens when you lazy-load the LCP image?
LCP gets worse. A lazy image only starts downloading once layout shows it is near the viewport, so the request goes out after CSS, scripts and layout instead of as soon as the HTML parser sees it, and it gets lower priority. On a slow connection that can add a second or more to the most visible element.
Q4When do you need <picture> instead of srcset on an <img>?
When the browser must not be left to choose. Use <picture> for format fallback, because a type source is skipped by browsers that cannot decode it, and for art direction, a different crop per breakpoint, because a matching media source is mandatory. For the same image at different sizes, srcset with sizes is enough.
Q5Why does an image cause layout shift, and how do you prevent it?
Because the browser does not know its height until the file arrives, so it lays out a zero-height box and reflows when the image loads. Declaring width and height attributes, or a CSS aspect-ratio, lets it reserve the box up front, and height: auto keeps that working for fluid images.
Q6AVIF, WebP or JPEG: what do you serve today?
AVIF first for photos, with WebP or JPEG as fallback, because AVIF is the smallest at equal quality and has been supported in all major browsers since early 2024. The cost is encoding time, so I generate it at build time or let an image CDN do it once and cache it. Logos and icons stay SVG.
Q7What does an image CDN do, and what can go wrong with caching?
It resizes, crops and re-encodes images on request from URL parameters, picks the format from the Accept header, and caches each variant at the edge. The classic bug is caching one format under a URL without Vary: Accept, so a browser that cannot decode AVIF gets AVIF. Another is allowing arbitrary widths, which lets anyone fill the cache and the transform bill; restrict widths to a fixed list.
- Three decisions per image: format (AVIF with a fallback), pixel size for the screen, and loading priority.
- The browser picks from
srcsetusingsizes× device pixel ratio; withoutsizesit assumes full viewport width. - Use
<picture>for format fallback and art direction, plainsrcsetfor the same image at different sizes. - The LCP image is eager, high priority and in the initial HTML; everything below the fold is lazy.
- Declare width and height or aspect-ratio on every image so the box is reserved and nothing shifts.
- Image CDNs and
next/imageautomate variants, but check AVIF is enabled, the hero is not lazy, and caching varies by format.