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
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 (
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
srcsetwith 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
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
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
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.