Ship one 4000px photo to every visitor and you're either blurring it on a laptop or making someone on a train wait eight seconds for a hero image they'll glance at once. A single <img> was built for a web where every screen was roughly the same size. That assumption broke a decade ago, and it's exactly why a professional web development team treats image markup as a performance surface, not an afterthought — the file you ship and the file the browser actually needs are rarely the same file.
TL;DR: srcset plus sizes let the browser pick the right-sized image file for the layout it's actually rendering. <picture> exists for two specific jobs — art direction (swapping crops per breakpoint) and format fallback (AVIF/WebP to JPEG) — not general responsiveness. Pair both with aspect-ratio to stop layout shift, and never lazy-load the image above the fold.
The srcset attribute on <img> doesn't pick an image — it gives the browser a menu of candidate files and lets the browser's own layout engine choose. The browser already knows the viewport size, the device pixel ratio, and (once you tell it) the rendered width of the image. That's more information than you have at write time, so handing the decision to the browser produces better results than hardcoding breakpoints in JavaScript.
Most of the time you'll use width descriptors (400w, 800w, 1200w), which tell the browser the actual pixel width of each file:
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1200.jpg 1200w,
photo-2400.jpg 2400w"
sizes="(min-width: 900px) 50vw, 100vw"
alt="A developer reviewing a layout on two monitors">
The src attribute still matters here — it's the fallback for any browser too old to understand srcset at all, so never omit it.
Pixel-density descriptors (1x, 2x, 3x) are simpler but less flexible: they assume the image renders at a single fixed CSS size and only vary by device pixel ratio (DPR), the ratio of physical pixels to CSS pixels on a given screen. Use them for fixed-size UI elements like logos and icons, where the display size never changes; use width descriptors for anything that scales with the layout, which is most content imagery.
Because srcset only lists candidate files — it never tells the browser how wide the image will actually render on the page. Without sizes, the browser has to guess, and its fallback guess is the full viewport width, which means a phone browser might select your largest file for an image that's actually rendering in a 300px sidebar. The sizes attribute is a set of CSS-length hints, evaluated in order, that tells the browser the rendered width at each condition: (min-width: 900px) 50vw, 100vw means "50% of viewport above 900px, full viewport below it." Get this value wrong — copy it from a different layout, say — and every width descriptor in srcset becomes useless, because the input feeding the selection logic is wrong.
<picture> gets reached for constantly as "the responsive image tag," but it solves exactly two problems that srcset and sizes can't: changing which image shows up (not just which size of the same image), and offering alternate file formats. If you just need the same photo at different resolutions, srcset alone is both simpler and correct.
Art direction means swapping the actual crop or composition at different breakpoints — a wide landscape shot on desktop, a tighter portrait crop on mobile where the wide version would shrink the subject to nothing:
<picture>
<source media="(max-width: 600px)" srcset="hero-tight-crop.jpg">
<source media="(min-width: 601px)" srcset="hero-wide-crop.jpg">
<img src="hero-wide-crop.jpg" alt="Team standing in front of the office building">
</picture>
Format fallback offers the same composition in progressively more-supported formats, letting the browser pick the first one it can decode — modern formats like AVIF and WebP compress better than JPEG, but you still need a universal fallback:
<picture>
<source type="image/avif" srcset="photo.avif">
<source type="image/webp" srcset="photo.webp">
<img src="photo.jpg" alt="Close-up of a mechanical keyboard">
</picture>
The order matters: the browser uses the first <source> whose type it supports, falling through to the plain <img> if none match.
A 400 / 800 / 1200 / 2400w ladder covers most layouts: 400w for thumbnails, 800w and 1200w for typical content-width images, 2400w as the 2x-DPR top end for a 1200px-wide display. Full-bleed heroes sometimes justify a 3200w step above that — but extra variants beyond what any real layout renders just add encode time without ever getting selected.
None of the above helps if the browser doesn't know the image's dimensions before it loads, because that's what causes layout shift: the page renders with no space reserved, the image arrives, and everything below it jumps. The fix is to always set width and height attributes (even though the actual rendered size is responsive) — modern browsers use them purely to compute the correct aspect-ratio and reserve space, not to force a fixed pixel size.
img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
object-fit: cover;
}
object-fit: cover is the piece people skip: once the box has a fixed aspect ratio, a source image that doesn't match it will squish unless you crop it to fill the box instead. Reserved space is what protects your Largest Contentful Paint (LCP) and layout shift (CLS) scores — the layout doesn't move once the real file decodes.
Native lazy loading (loading="lazy") is almost always the right default: it defers off-screen images until the user scrolls near them, cutting initial page weight for free. The one image that should never carry it is your Largest Contentful Paint candidate — typically the hero or the first content image above the fold. Lazy-loading it deprioritizes the exact resource your LCP metric is timing — the single most common self-inflicted regression in this whole topic.
A short reference for which tool actually applies:
| I need to... | Use this |
|---|---|
| Serve the same image at different resolutions | srcset + sizes on <img> |
| Show a different crop/composition per breakpoint | <picture> with media on <source> |
| Offer AVIF/WebP with a JPEG fallback | <picture> with type on <source> |
| Show one small, fixed-size image (icon, logo) | Plain <img>, no responsive markup needed |
src as a safety net for crawlers, extensions, and reader modes that only read the base attribute.sizes from a different layout is the single most common cause of "why is this loading the huge file on mobile."Yes, unless the image always renders at exactly 100% of viewport width — otherwise omitting sizes makes the browser assume full width and pick an oversized file.
Width descriptors (800w) describe the file's pixel width and work with sizes for layouts that reflow; pixel-density descriptors (2x) assume one fixed display size, so they fit icons and logos, not content that resizes.
No — <picture> is a wrapper that still requires an <img> inside it as the fallback; it adds <source> options in front, it doesn't replace it.
Almost always a mismatched aspect-ratio without object-fit: cover — the box forces a ratio the source doesn't have, and without object-fit the browser stretches rather than crops.
No. The hero or LCP candidate should load eagerly; lazy-loading it delays the exact resource your Largest Contentful Paint metric measures.
A 2400w or 2x top-tier file already covers most 1200px-wide 2x displays; a dedicated 3x tier is usually only worth it for full-bleed hero imagery.
They still exist, but srcset/sizes handle the same problem without server-side negotiation, which is why most teams never implement Client Hints at all.
Once you separate the two jobs, none of this is complicated: srcset and sizes answer "which resolution," <picture> answers "which image or which format." Both are native HTML, both degrade gracefully, and neither needs a build step to keep working in ten years — the actual argument for using the markup the spec gives you instead of a client-side image component.