Responsive Images: srcset, picture & Modern Formats
Serving the right image at the right size and format is one of the highest-leverage performance and UX decisions a front-end team can make.
8 min read
The full lesson
Images make up more than half of all bytes transferred on the average web page. Get them wrong and you inflict a measurable layout-shift penalty, push your Largest Contentful Paint (LCP) past the 2.5-second threshold, and waste real money for users on metered data plans. Get them right and you win on performance, accessibility, and cost at the same time — which is why responsive image implementation is one of the highest-ROI decisions a front-end team can make.
Why a Single Image Source Is Never Enough
A 1400-pixel hero photo looks great on a desktop Retina display. Served unchanged to a 375-pixel phone on a cellular connection, it transfers four to eight times more pixels than the screen can show. The browser still has to decode and rasterize the entire bitmap, burning battery and memory in the process.
Pixel density multiplies the problem. A 2× device at 390 CSS pixels needs a 780-pixel-wide source to look sharp. That same file served to a 1× laptop at 1280 pixels is undersized and blurry. No single image satisfies every viewport at both 1× and 2× density without either wasting bandwidth or looking soft.
The responsive images spec — srcset, sizes, and the picture element — solves these two problems with two distinct tools. Mixing them up is the most common implementation mistake.
Resolution Switching with srcset and sizes
Resolution switching serves the same image at different widths. Use it when you want the browser to pick the most efficient source. You are not changing the crop or subject — just the file dimensions.
<img
src="hero-800.webp"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
alt="Aerial view of a coastal city at golden hour"
width="800"
height="533"
/>
Breaking this down:
srcsetwithwdescriptors tells the browser the intrinsic pixel width of each candidate file. Never usexdescriptors here — they ignoresizesentirely and choose based on pixel density alone, ignoring viewport width.sizestells the browser how wide the image will render at various viewport widths, evaluated left to right. The final value (800px) is the default. Match these values to your actual CSS layout — if the image spanscalc(100vw - 2rem)on mobile, say so.srcis the fallback for browsers that do not understandsrcset. Put a mid-range source here.widthandheightattributes are mandatory. Without them the browser cannot reserve layout space before the image loads, which causes cumulative layout shift (CLS). Set them to the intrinsic dimensions of thesrcimage — CSS can scale it freely while the reserved slot holds.
The browser’s selection algorithm weighs viewport width, device pixel ratio, and — in modern implementations — network quality from the Network Information API. You supply the candidates; the browser chooses.
Art Direction with the picture Element
Art direction is a different problem. You want to serve a fundamentally different crop or composition depending on viewport size. The picture element gives the browser conditional source slots with full media-query control.
<picture>
<source
media="(max-width: 599px)"
srcset="hero-portrait-400.webp 400w, hero-portrait-800.webp 800w"
sizes="100vw"
type="image/webp"
/>
<source
media="(min-width: 600px)"
srcset="hero-landscape-800.webp 800w, hero-landscape-1600.webp 1600w"
sizes="(max-width: 1200px) 50vw, 800px"
type="image/webp"
/>
<img
src="hero-landscape-800.jpg"
alt="Aerial view of a coastal city at golden hour"
width="800"
height="533"
/>
</picture>
Key rules:
- The
imginsidepictureis not optional. It is the fallback, carries thealttext, and holds thewidth/heightattributes. Screen readers announce theimgalt, not thepicturewrapper. sourceelements are evaluated top to bottom. The first match wins. Order from most restrictive to least.- Each
sourcecan carry its ownsrcsetandsizesfor resolution switching within an art-directed crop. - Use
pictureonly when the composition genuinely changes. Do not use it just for format selection — that is what thetypeattribute is for.
Format Selection: AVIF, WebP, and the JPEG/PNG Fallback
Modern image formats deliver dramatically smaller files at the same visual quality:
| Format | Browser support (2026) | Typical savings vs. JPEG | Best for |
|---|---|---|---|
| AVIF | 96 %+ (all evergreen) | 40–60 % | Photos, gradients, HDR |
| WebP | 97 %+ | 25–35 % | Photos, illustrations, transparency |
| JPEG XL | Experimental (flag-gated) | 35–50 % | Archival, high-fidelity photos |
| PNG | Universal | — | Lossless screenshots, pixel art |
| JPEG | Universal | baseline | Fallback for all photos |
The standard pattern is AVIF first, WebP second, JPEG/PNG fallback — all inside a picture element:
<picture>
<source type="image/avif" srcset="photo-800.avif 800w, photo-1600.avif 1600w" sizes="..." />
<source type="image/webp" srcset="photo-800.webp 800w, photo-1600.webp 1600w" sizes="..." />
<img src="photo-800.jpg" srcset="photo-800.jpg 800w, photo-1600.jpg 1600w" sizes="..." alt="..." width="800" height="533" />
</picture>
The browser picks the first source whose type it supports. A browser that does not support AVIF skips to WebP. One that does not support WebP falls back to the img JPEG chain.
Lazy Loading and Fetch Priority
Responsive images interact directly with Core Web Vitals through two HTML attributes.
loading="lazy" defers off-screen image fetches until the user scrolls near them. Apply it to every image that is not visible in the initial viewport. All modern browsers support it, and it degrades gracefully.
fetchpriority="high" tells the browser to fetch this image early in the preload queue. Apply it to the image most likely to be your Largest Contentful Paint element — the hero image, the first product photo, the above-the-fold illustration. Do not apply it to multiple images or the hint loses meaning.
<!-- LCP image: prioritize, never lazy-load -->
<img
src="hero.webp"
srcset="hero-800.webp 800w, hero-1600.webp 1600w"
sizes="100vw"
fetchpriority="high"
alt="..."
width="1600"
height="900"
/>
<!-- Below-fold image: lazy-load -->
<img
src="card-thumb.webp"
srcset="card-thumb-400.webp 400w, card-thumb-800.webp 800w"
sizes="(max-width: 600px) 50vw, 25vw"
loading="lazy"
alt="..."
width="400"
height="300"
/>
A common mistake is adding loading="lazy" to the LCP image on the assumption that lazy loading is always good. It is not. It delays the most important image and can push LCP past 4 seconds on slower connections.
Decoding, Aspect Ratio, and Layout Stability
decoding="async" tells the browser it can decode the image off the main thread, reducing jank. It is safe to apply universally and never delays rendering.
Aspect ratio reservation matters more than most teams realize. When width and height attributes are present, the browser uses them to calculate an intrinsic aspect ratio and reserves the exact layout slot before the image loads. This eliminates CLS from image loading entirely — one of the most impactful CLS fixes available for image-heavy pages.
If your image is CSS-scaled (width: 100%; height: auto), the browser still uses the width/height attributes to compute the ratio. You do not need to set a CSS aspect-ratio property unless you need to override the intrinsic ratio.
The Modern Build Pipeline Approach
Hand-authoring srcset strings for dozens of images is error-prone and hard to maintain. In 2026, the right approach is to treat image optimization as an automated build step.
- Astro —
import { Image } from 'astro:assets'generates AVIF + WebP variants, writes the correct srcset/sizes, and enforcesaltat build time. Zero hand-authoring required. - Next.js —
next/imagehandles format negotiation server-side via the image optimization API, automatically serving AVIF or WebP based on theAcceptheader. - CDN-based — Cloudinary, imgix, and Cloudflare Images accept a single source file and serve the optimal format and width via URL parameters, deferring all transcoding to the edge.
For static sites not using a framework, a sharp-based build script (a Vite plugin, an Eleventy transform, or a standalone script) can generate all variants from a source directory. The key point: no human should ever be hand-resizing images or manually writing srcset strings.
Do
Set width and height attributes on every img element to reserve layout space and prevent CLS. Use fetchpriority=“high” on the LCP image only. Use AVIF as the leading source in your picture format waterfall. Generate all image variants in a build pipeline rather than manually.
Don't
Add loading=“lazy” to your LCP hero image — it delays the most critical render. Use pixel-density x descriptors with srcset when width w descriptors exist — x descriptors ignore viewport width and sizes entirely. Use picture just for format switching without art direction — that adds markup complexity for no gain over srcset plus type on source elements. Serve a 1400px wide image to a 375px mobile screen because ‘retina requires it’ — a 2x mobile screen only needs 750px.
Accessibility Considerations
Responsive images have two accessibility responsibilities.
Alt text belongs on the img element, always — not on picture or source. Write descriptive alt text for meaningful images. For decorative images, set alt="" so screen readers skip them. Never omit the attribute. An img without alt is announced by VoiceOver and NVDA as the filename or URL, which is meaningless to most users.
Color and contrast in images become relevant for text-over-image compositions. If you overlay text on a hero image, ensure sufficient contrast at every art-directed crop. A tightly cropped mobile version that shifts to a lighter background region may suddenly drop below the WCAG 2.2 AA threshold — 4.5:1 for normal text, 3:1 for large text. Test overlays at every responsive breakpoint, not just the desktop version.
Putting It Together: A Decision Tree
When implementing a responsive image, ask these questions in order:
- Does the crop or subject change at different viewports? Use
picturewithmediaonsourceelements for art direction. - Do you need multiple formats (AVIF, WebP, fallback)? Add
typetosourceelements insidepicture. - Is this a resolution switch only (same crop, different size)? Use
imgwithsrcset(w descriptors) andsizes. - Is this the LCP image? Add
fetchpriority="high"and omitloading="lazy". - Is this below the fold? Add
loading="lazy"anddecoding="async". - Does the
imghavewidthandheight? If not, add them.
This sequence keeps markup minimal. Most images need only srcset, sizes, width, height, and the right loading attributes — not a full picture wrapper.