# Responsive images with srcset and sizes: a practical guide

> srcset lists candidate images and sizes tells the browser how wide the image will render. Omit sizes and the browser assumes full viewport width and downloads the largest candidate on every screen - which is the exact opposite of what you wanted.

Source: https://rankcert.com/blog/responsive-images-guide
Published: 2026-08-29 · Updated: 2026-09-02

---


Responsive images exist to solve one problem: a phone should not download the image a 27-inch monitor needs. Two attributes do almost all the work, and one of them is routinely left out.

## srcset and sizes

```html
<img
  src="/hero-1200.webp"
  srcset="/hero-600.webp 600w, /hero-1200.webp 1200w, /hero-2400.webp 2400w"
  sizes="(max-width: 768px) 100vw, 1200px"
  width="1200" height="630"
  alt="..."
/>
```

**`srcset`** lists candidates with their intrinsic widths. The `w` descriptor is the image's actual pixel width, not the size it displays at.

**`sizes`** tells the browser how wide the image will *render* at each breakpoint. Without it, the browser assumes `100vw` and picks the largest candidate on every screen - so adding `srcset` without `sizes` can make things worse than having neither.

**`src`** is the fallback for anything that does not understand `srcset`.

**`width` and `height`** are not optional. They let the browser reserve space before the image arrives, which is the single biggest fix for layout shift.

The browser chooses before layout is computed, using `sizes` as your declaration of intent. That is why it must be maintained when your CSS changes - a stale `sizes` is a silent regression.

## Why the browser sometimes picks bigger

Two reasons that look like bugs and are not:

**Device pixel ratio.** On a 2× display, an image rendering at 600 CSS pixels needs a 1200px source. The browser multiplies for you.

**Already cached.** If a larger candidate is already in cache, browsers will use it rather than fetch a smaller one. Sensible, and it makes testing confusing - always test with an empty cache.

## picture, for a different job

`srcset` chooses between **sizes of the same image**. `<picture>` chooses between **different images or formats**.

```html
<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg" width="1200" height="630" alt="..." />
</picture>
```

Use `<picture>` for format fallbacks, and for art direction - a wide crop on desktop and a square crop on mobile, which is a genuinely different image rather than a smaller one.

Do not use it where `srcset` would do. It is more markup for the same result.

## How many candidates

Three is usually right: roughly 1×, 2× and one in between. More candidates means more files to generate, store and invalidate, for diminishing returns.

Pick widths from your actual breakpoints rather than round numbers. If your container is 1200px at most, a 2400px candidate covers retina and nothing above it is useful.

Free tool: [Image Resizer](https://rankcert.com/tools/image-resizer) - Resize PNG, JPG, WebP and GIF images to exact pixel dimensions with aspect ratio locked. Runs entirely in your browser - no upload, no queue, no watermark.

## Loading priority

The LCP image gets `fetchpriority="high"` and a preload, and must never be lazy. Everything below the fold gets `loading="lazy"`.

Lazy-loading the hero is a common self-inflicted wound: it delays the LCP by a full round trip because the browser will not start the fetch until layout tells it the image is near the viewport.

<Callout>
If you add srcset and your images get bigger rather than smaller, you left out sizes. That is the failure mode, and it is invisible unless you check transferred bytes at a narrow viewport.
</Callout>

<Cta />
