Start a project

Article · Web performance · Images · Responsive · 12 min read

Responsive images with srcset and sizes

A single image file is rarely the right size for every screen. srcset lists the versions you have; sizes tells the browser how wide the image will be in the layout at different viewport widths; the browser then picks a file. If you omit sizes, or guess it, a phone can still download a desktop original. A WebP or AVIF conversion without srcset has the same problem: a smaller codec in a file that is still two or three times too wide.

On this page 10 sections
  1. One image file is rarely the right size
  2. srcset, sizes and the slot the image will fill
  3. Width descriptors versus density descriptors
  4. Art direction with picture
  5. Formats, compression and what to measure
  6. Lazy loading, fetch priority and LCP images
  7. How to check what the browser actually requested
  8. A default for articles, cards and full-bleed heroes
  9. Key takeaways
  10. Frequently asked questions

I generate srcset and sizes for images on leej.uk from the real layout slot, and I built the srcset Generator and the srcset sizes Calculator so I could stop guessing the attributes. The method below is how I write them, how I treat the LCP image, and how I check what the browser actually requested.

One image file is rarely the right size

A hero that is 1,280 pixels wide on a desktop, 840 on a tablet, and the full viewport on a phone is three different download problems, even before you count device pixel ratio. A 2x phone in a 320 CSS-pixel slot still only needs a file around 640 pixels wide, not the original 3,000-pixel export from a camera.

If you ship one large file:

The HTML for responsive images exists so you can offer a set of candidates and describe the slot. Compression, which the Image Compressor and the Image Quality Explorer help with, is the other half. You need both: a well-compressed file of the wrong width is still the wrong file.

This is the same instinct as designing for the Apple Watch or any small screen: decide the size the image will actually occupy, then supply pixels for that size, not for the largest device you can imagine.

srcset, sizes and the slot the image will fill

For a flexible image, the usual pattern is a srcset of width descriptors and a sizes attribute that describes the slot:

<img
  src="/images/harbour-800.jpg"
  srcset="
    /images/harbour-400.jpg 400w,
    /images/harbour-800.jpg 800w,
    /images/harbour-1200.jpg 1200w,
    /images/harbour-1600.jpg 1600w"
  sizes="(min-width: 1024px) 640px, 92vw"
  width="1600"
  height="900"
  alt="Whitby harbour at low tide, looking towards the abbey.">

sizes is the attribute people invent. If the desktop layout actually shows the image at 640 CSS pixels, write 640px for that breakpoint, not 100vw. If on smaller screens the image is almost the full viewport plus a page margin, measure that margin and write something like 92vw, rather than 100vw out of habit. The srcset sizes Calculator is a way to turn a layout into that string. The srcset Generator will then pair it with a set of file widths.

The HTML specification and MDN’s img documentation are the references for the syntax. The important idea is: srcset is supply, sizes is demand.

Width descriptors versus density descriptors

You will also see density descriptors (1x, 2x, 3x):

<img
  src="/images/icon-32.png"
  srcset="/images/icon-32.png 1x, /images/icon-64.png 2x"
  width="32"
  height="32"
  alt="">

This is the right tool when the image’s CSS size is fixed: an icon, a logo in the header, an avatar at 48 CSS pixels. The browser picks by device pixel ratio. You do not need sizes, and you should not mix x and w descriptors on the same srcset.

For everything that stretches with the layout — heroes, article photographs, cards, full-bleed images — use width descriptors and sizes. A 2x pair cannot describe “this is half the viewport on a phone and 640 pixels on a desktop”. If you only offer 1x and 2x of a desktop-sized file, a phone still has to choose among files that are too wide.

A reasonable set of widths is a geometric series around the slots you actually have, for example 400, 800, 1,200 and 1,600 for an article photo, not twelve barely different copies. Produce them from the same master, and keep the aspect ratio identical unless you are doing art direction.

Art direction with picture

srcset on one <img> is for the same picture at different resolutions. When the picture itself should change — a cropped portrait on a phone, a wide landscape on a desktop, or a different focal point — use <picture> with media conditions:

<picture>
  <source
    media="(max-width: 599px)"
    srcset="/images/harbour-tall-400.jpg 400w, /images/harbour-tall-800.jpg 800w"
    sizes="92vw">
  <source
    media="(min-width: 600px)"
    srcset="/images/harbour-wide-800.jpg 800w, /images/harbour-wide-1600.jpg 1600w"
    sizes="(min-width: 1024px) 640px, 92vw">
  <img
    src="/images/harbour-wide-800.jpg"
    width="1600"
    height="900"
    alt="Whitby harbour at low tide, looking towards the abbey.">
</picture>

The last element must be an <img>, with alt, and preferably with width and height. It is the fallback and the element that CSS hooks onto. The <source> elements offer alternative files; they are not a substitute for the img.

<picture> is also how you offer modern formats with a fallback:

<picture>
  <source type="image/avif" srcset="/images/harbour-800.avif 800w, /images/harbour-1600.avif 1600w" sizes="(min-width: 1024px) 640px, 92vw">
  <source type="image/webp" srcset="/images/harbour-800.webp 800w, /images/harbour-1600.webp 1600w" sizes="(min-width: 1024px) 640px, 92vw">
  <img
    src="/images/harbour-800.jpg"
    srcset="/images/harbour-800.jpg 800w, /images/harbour-1600.jpg 1600w"
    sizes="(min-width: 1024px) 640px, 92vw"
    width="1600"
    height="900"
    alt="Whitby harbour at low tide, looking towards the abbey.">
</picture>

Keep sizes consistent across the sources, and keep the art-direction crops consistent with the alt text. If the mobile crop no longer shows what the alt describes, the alt is wrong, or the crop is. That is an accessibility issue as well as a layout one, in the same spirit as accessible interfaces: the meaning has to survive the format.

Formats, compression and what to measure

AVIF and WebP often shrink a photograph compared with JPEG, at a similar visual quality, and they are widely supported in current browsers. They are not a reason to skip srcset. A 2,000-pixel AVIF in a 360-pixel slot is still a waste. Offer a set of widths in the modern format, and a JPEG (or PNG, for graphics that need it) fallback in the img.

Measure the result, not the rumour of a format:

A performance budget should cap the LCP image and the typical article’s image bytes, so a new hero cannot walk in at four megabytes because “AVIF is efficient”.

Lazy loading, fetch priority and LCP images

loading="lazy" tells the browser it may defer images that are not yet near the viewport. That is appropriate for images further down an article, and it is usually wrong for the LCP image: the hero, the first large photograph, the main product shot. Lazy-loading that element can delay the largest paint on purpose. Leave the LCP image eager (the default), and consider fetchpriority="high" on it so the browser knows it competes well with other early requests.

<img
  src="/images/harbour-800.jpg"
  srcset="..."
  sizes="..."
  width="1600"
  height="900"
  fetchpriority="high"
  alt="Whitby harbour at low tide, looking towards the abbey.">

Below-the-fold images can take loading="lazy" and, if you like, a lower fetch priority. Do not lazy-load images that are already in the first viewport on a common phone width; test at a narrow width, not only on a tall desktop window.

decoding="async" can help the browser decode without blocking the main thread; it is a modest extra, not a substitute for a correctly sized file.

Preload (<link rel="preload" as="image">) is sometimes useful for the LCP image, but if you preload you must preload the file the browser will actually pick, including format and width, or you can download twice. A well-written srcset and sizes in the first HTML, with fetchpriority="high", is the simpler default. If you do preload, keep it in sync with the markup, and put the <img> in the first response, not only after JavaScript, or a crawler and a cold load will not see it in time.

How to check what the browser actually requested

Do not trust the markup alone.

  1. Open the Network panel, filter to images, and load the page at the viewport you care about, with caching disabled for the test, on a representative pixel ratio. Note the file that was requested, its dimensions, and its transfer size.
  2. In the console, document.querySelector('img.hero').currentSrc (or the appropriate selector) shows the chosen URL after the decision.
  3. Change the viewport and reload, or use a device toolbar, and confirm a smaller slot requested a smaller width. Resizing alone may not always pick a new candidate if a larger file is already cached; a reload on a clear cache is the fair test.
  4. Compare the chosen file’s intrinsic width with devicePixelRatio * slot width. If you are consistently downloading a file twice as wide as that, sizes is probably too large, or your srcset jumps too aggressively.
  5. For the LCP image, confirm in a performance trace or Lighthouse that the LCP element is that image, that it was not lazy-loaded, and that it was discovered in the first HTML. This is the same “look at the recording” habit I use when finding performance problems with Instruments.

If the browser picked a surprisingly large file, fix sizes before you add more widths. Extra candidates will not help a demand string that claims the image is the full viewport when it is not.

A default for articles, cards and full-bleed heroes

These are the defaults I start from, then I measure.

An article photograph in a column that is, say, 640 CSS pixels on a desktop and almost full-width on a phone: one <img> with width-descriptor srcset (for example 400, 800, 1,200, 1,600), a sizes string that matches that column, width and height, a mid-sized src, and loading="lazy" only if the image is definitely below the fold. Offer AVIF or WebP via <picture> if you already have those derivatives.

A card image in a grid of two or three columns: smaller slots, so smaller widths, and a sizes string that divides the viewport (for example (min-width: 1024px) 300px, (min-width: 640px) 45vw, 92vw). Card images are where a copied hero sizes="100vw" does the most damage, because you may have several of them.

A full-bleed hero that is also the LCP element: widths up to the largest layout you support, a sizes of 100vw if it really is full bleed, no lazy loading, fetchpriority="high", reserved space, and the markup in the first HTML response. If the hero crops differently on a phone, use <picture> art direction, and keep the reserved aspect ratio from jumping.

Icons and logos at a fixed CSS size: density descriptors, or a single SVG. Do not run them through the same 1,600-pixel pipeline as a photograph.

Write alt for all of these as if the file failed to load, which it sometimes will.

Key takeaways

If you want a second pair of eyes on a hero that is still two megabytes on a phone, tell me about it.

Frequently asked questions

What is the difference between srcset and sizes?

srcset lists the files you have, and the width (or density) of each file. sizes tells the browser how wide the image will be in the layout at different viewport widths, so it can choose among those files before layout is complete. Supply and demand, respectively. One without a careful other still leads to an oversized download.

Should I use width descriptors or pixel density descriptors?

Use width descriptors (800w) plus sizes for images that change width with the layout. Use density descriptors (1x, 2x) for images that stay a fixed CSS size, such as icons. For heroes, article photographs and cards, width descriptors are the right default.

How do I write a sizes attribute that is not a guess?

Measure the CSS width of the slot at your breakpoints, including margins and grid columns, and write those lengths into sizes. A calculator helps, but it still needs the real layout as input. Then confirm, in the Network panel, that a narrow viewport requests a smaller file than a desktop viewport.

When should I use the picture element?

Use it when you need art direction (a different crop or image at a breakpoint) or when you want to offer AVIF or WebP with a JPEG or PNG fallback. The last child should still be an img element with alt text and dimensions. If you only need resolution switching of the same picture, srcset and sizes on the img are enough.

Is WebP or AVIF enough without srcset?

No. A modern format can reduce bytes for a given width, but it does not choose a width. Without srcset (and a correct sizes), the browser still has only one file, which is often the desktop original. Offer several widths in the modern format, and keep a fallback in the img.

Written by Lee Johnson. Published 10 November 2026.

All articles · Swift A–Z · Apps

Questions, or an app in mind?

Let's bring your ideas to life. Whether you're envisioning a game-changing Apple Watch app or a transformative iOS experience, I'm here to make it happen.

Start a project