Responsive images

Serve the right image file for the screen it lands on, using srcset, sizes and picture.

08of 22
≈ 11Min read
8Parts

The problem

One image file has to serve a phone at 390 points wide with a 3× screen, a laptop at 1440, and a 5K monitor. Send the largest and you waste most of a phone's data on pixels it cannot show. Send the smallest and the desktop gets something blurred.

The browser is the only party that knows the screen size, the pixel density, the window width and the network. srcset gives it the choice.

srcset with widths

List the files with the real pixel width of each one, using the w descriptor. Then tell the browser how wide the image will be drawn with sizes, because it has to choose before it has done layout.

  • 400w is a fact about the file: it is 400 pixels wide. It is not a condition.
  • sizes is a list of media conditions with the width the image will occupy, and a final value with no condition as the default.
  • src stays as the fallback for anything that does not understand srcset.
three files, one image
<img src="/media/hall-800.jpg"
     srcset="/media/hall-400.jpg 400w,
             /media/hall-800.jpg 800w,
             /media/hall-1600.jpg 1600w"
     sizes="(max-width: 700px) 100vw, 50vw"
     alt="A corridor with a concealed door"
     width="1600" height="900">

How the browser chooses

On a 390-point phone with a 3× screen, sizes resolves to 100vw = 390 CSS pixels, which needs 1170 device pixels — so it takes the 1600 file. On a 1200-pixel laptop at 1×, 50vw = 600, so it takes the 800 file.

It may also take a smaller one on a slow connection, and it will never downgrade an image it has already cached. That last rule surprises people testing in devtools: reload with the cache disabled.

A wrong sizes is worse than no srcset

If you write sizes="100vw" for an image that is actually 400 pixels wide in a column, the browser downloads the biggest file on every screen. Keep sizes honest — it must describe the CSS width, not the ambition.

srcset with densities

When the image is always drawn at one fixed size — an avatar, a logo, an icon — you do not need sizes. Use the x descriptor instead.

a fixed-size image
<img src="/media/avatar-96.png"
     srcset="/media/avatar-96.png 1x,
             /media/avatar-192.png 2x,
             /media/avatar-288.png 3x"
     alt="Ulvi Shabanbayli" width="96" height="96">

Never mix w and x descriptors in one srcset — pick the one that matches how the image is sized.

picture, for a real choice

srcset picks between different sizes of the same picture. <picture> picks between genuinely different files: another format, or a different crop.

The browser takes the first <source> whose type and media it can satisfy. The <img> at the end is both the fallback and the element that is actually styled — all your CSS, the alt and the dimensions belong on it.

modern formats with a fallback
<picture>
  <source srcset="/media/hall.avif" type="image/avif">
  <source srcset="/media/hall.webp" type="image/webp">
  <img src="/media/hall.jpg" alt="A corridor with a concealed door"
       width="1600" height="900">
</picture>

Art direction

On a wide screen a panorama works; on a phone the subject is a speck. Art direction means cropping differently, not just scaling — and that is the other job <picture> does.

a different crop on small screens
<picture>
  <source media="(max-width: 700px)"
          srcset="/media/hall-square-800.jpg"
          width="800" height="800">
  <img src="/media/hall-wide-1600.jpg" alt="A corridor with a concealed door"
       width="1600" height="900">
</picture>
Put width and height on the source too

When the crops have different shapes, giving each source its own dimensions lets the browser reserve the correct space for whichever one it picks.

Dark mode

a diagram that suits both themes
<picture>
  <source srcset="/media/diagram-dark.svg" media="(prefers-color-scheme: dark)">
  <img src="/media/diagram-light.svg" alt="How the frame is fixed into the opening"
       width="900" height="500">
</picture>

Priority

  • loading="lazy" on everything below the fold. Never on the first image.
  • fetchpriority="high" on the single most important image, usually the one at the top.
  • <link rel="preload" as="image" imagesrcset="…" imagesizes="…"> in the head when even the parser finds it too late.
  • Serve the image at the size you actually display it. A 4000-pixel photograph in a 600-pixel column is the most common performance problem on the web.

Practice