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.