Oliver Joseph Ash

Full-stack web developer

Pitfalls of the img element for serving responsive images

At the Guardian we wanted to serve responsive images based solely on their rendered width in the page layout at each breakpoint. We explicitly decided not to respond to DPR, preferring to save on user bandwidth over increased image quality. Our first version of this used the img element in combination with the sizes and srcset, but we soon noticed holes in our implementation, which led to high DPR devices unintentionally incurring larger image downloads. In this post I will explain how we used the picture element to workaround this, and how—once we decided to serve retina images—this enabled us to optimise for them separately.

For each size we provide a corresponding source image with a matching width. For example, at viewport widths greater or equal to 600px, the image appears at 600px wide in our layout. At all smaller viewport widths, the image appears at 300px wide in our layout. The code for this looks like so:

<img sizes="(min-width: 600px) 600px, 300px"
     srcset="600.jpg 600w, 300.jpg 300w" />

This works fine for “low DPR” devices, but we noticed that high DPR devices could greedily download a larger image. For example, on a device with a pixel ratio of 2 and a viewport width of 300px, the browser would select the correct size (300px) but the incorrect source (600.jpg 600px).

This was unintentional: our usage of sizes and srcset was strictly for serving images based on breakpoint width, not DPR!

We had forgotten that the browser’s formula for selecting the img’s source accounted for the device’s DPR. Doh!

Let’s remind ourselves how the browser selects a source:

selected size
first size with a media condition that evaluates true
ideal width
dpr * selected size
selected source
a source in srcset with a width descriptor closest to the ideal width

(This formula is not standardised, but it is seemingly consistent across the web platform.)

So how can we serve images that vary their width by breakpoint and not DPR?

The answer was the picture element, which allows you to provide multiple source elements, each with their own sizes and srcset attributes. Significantly, the source element also has a media attribute which allows you to guard the element from usage with a media condition. Using the source element we were able to split our sizes and srcset attributes by breakpoint. This meant high DPR devices could only choose from images available at the current breakpoint.

<picture>
  <source media="(min-width: 600px)"
          sizes="600px"
          srcset="600.jpg 600w" />
  <source sizes="300px"
          srcset="300.jpg 300w" />
  <img />
</picture>

(In many cases, content authors vary their image widths based on intervals instead of breakpoints, in which case it is safe to use a single img element with sizes/srcset attributes.)

This structure also gives us increased flexibility. For example, if we did decide to start serving retina images to compatible devices (which we did), we could serve an additional source and match only high DPR devices. This has the added benefit of allowing us to optimise separately for images we know will only be used on high DPR devices.