While at imgix, I spent a lot of time thinking about responsive images. It’s a very interesting problem that pays big dividends when solved correctly. By serving up the correctly sized images, you’re able to send less data across the wire to the client, resulting in a faster experience.
This approach uses generated
srcset values to give a bucket of potential images to the browser,
and then lets the browser choose which one to serve. (For a primer on
sizes, read the definitive post by Eric Portis.)
Even this right-in-most-cases-approach is difficult to implement correctly. I believe the agreed upon nomenclature is “terrible”:
Media-query-based responsive image source-picking is terrible because while most responsive designers have settled on varying a page’s layout based on one variable (viewport width), when dealing with responsive images, we’re really concerned with three variables:
- The rendered size (in CSS pixels) of the image on our layout
- The screen density
- The dimensions of the variously-sized files at our disposal
Using a solution like imgix eliminates the last concern in that list: we can generate derivative images from a single master at arbitrary dimensions on-the-fly.
Now we only have to worry about the first two points: sizing our image correctly in our layout and the screen density. But these are two things that we can defer to the browser if we declare our HTML correctly.
By simply declaring a bunch of image sizes, we’re able to let the browser make that decision.
A quick note about pixel perfection. This approach assumes that the implementing designer or front-end engineer cares about pixel perfection. More people should care about pixel perfection. If the image is not the exact perfect size, the client must do some amount of interpolation on the image to get the pixels delivered to map to the pixles on the screen. Sharp lines become blurry, and detail is lost. (For more on the performance implications of doing client-side resizes, check out this talk by Tim Kadlec.)
When we don’t serve images with pixel perfection, we are also by definition sending more or less data than required to display the image. Some pixels will be thrown out, or we’ll have to interpolate image data when the browser scales up the image.
Furthermore, each successive resize and compression slowly kills image quality. By actively working to reduce the number of times images are resized, whether on the client or server, we ensure that images are served at the highest quality.
A Sea of Markup
experiences, all of the imperative work done to request a new size
of the image must now be declared in the markup. To do this, we turn to
srcset attribute on the
<img> and we put it through its paces:
We declare a new image for just about every possible size we think we’ll need.
The result looks something like this:
In the biggest case, this results in over 100
srcset rules! This
can make a single image tag weigh more than 8 KB in HTML. To anyone keeping
an eye on payload sizes, that sounds like an intractable proposition.
But there are a few wrinkles here that make this not the worst idea:
- This content compresses extremely well. The above example gzips down to 736 bytes. The amount of data that needs to be sent over the wire to convey a very large set of possibilities is very small.
- This allow the browser to request only the amount of pixels it needs. This can help make up for the dirty feeling that we’ve got lots of markup, because we can guarantee that fewer image bytes get sent on the wire.
Wrangling with sizes
There is a bit of information that we can use to whittle down the number
srcset rules we define, and that is the
sizes attribute. The
attribute tells the image how to behave at different viewport widths. For our purposes,
it also defines the bounds of what we need to generate.
When not specified,
sizes is interpreted as
100vw or “100% of the viewport width.”
Case and point: if we set
"(min-width: 540px) 540w, 100vw" we can prune
srcset rule we generate. This
sizes example says “When the viewport is 540px
or wider, just use the image that is defined by
540w. Otherwise, fall back to 100%
of the viewport width.” From this we can infer that we will never need an image
wider than 540w and can remove those from our
srcset rules. Boom, savings.
Just to be nice, we should generate rules for
1080w (@2x DPR) and
1620w (@3x DPR) images.
Targeting srcset rules to devices
Rather than just iterate through a bunch of pixel widths, it’s better if we keep in
mind how responsive images are implemented in practice. There’s one pattern in
particular that’s important to pay attention to: more often than not, mobile devices use
100vw) images. We should define rules that exactly target these devices
to provide for a pixel-perfect experience.
We can account for this by including rules for known device widths in the markup,
srcset rules for the following:
640wfor the iPhone SE. 320 logical pixels wide @ 2x
750wfor the iPhone 6S. 375 logical pixels wide @ 2x
1242wfor the iPhone 6S Plus. 414 logical pixels wide @ 3x
The finished product
Putting this all together, we get HTML that looks something like this:
And our image looks the same:
This markup is no bigger than it needs to be to deliver a statically-defined, fully-responsive experience that provides pixel-pefect images to all mobile devices currently in existence. It compresses well, since much of the information is repeated.
So for including a few more bytes of gzipped HTML, we’re able to shave off 10 KB of image weight, provide pixel-perfect images, and not need to send our images to the mobile GPU for resizing saving (some negligible amount of) battery life.
Thus concludes this novel way of using