Boiling the Ocean with Markup
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.
The problem is that there’s plenty of different ways to change the resolution of an image for the client using JavaScript, CSS, and/or HTML. But there’s only one way that I’ve found that might be the closest to right-in-most-cases, and it’s a bit unintuitive. I cheekily refer to it as the boiling the ocean with markup approach.
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 srcset
and 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.
Pixel Perfection
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
To sidestep the requirement of JavaScript for delivering pixel-perfect image
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
the 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:
<img srcset="https://assets.imgix.net/flower.jpg?w=50 50w,
https://assets.imgix.net/flower.jpg?w=100 100w,
https://assets.imgix.net/flower.jpg?w=150 150w,
...
https://assets.imgix.net/flower.jpg?w=5200 5200w,
https://assets.imgix.net/flower.jpg?w=5260 5260w"
src="https://assets.imgix.net/flower.jpg?w=540"
alt="A white flower"
/>
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.
- Browsers are very good at parsing markup. Keep in mind this is markup, not JavaScript. The browser will have parsed this and preflighted the image it needs before it even begins executing the JavaScript. Although the amount of markup seems explosive, it still delivers a better experience than the JavaScript alternative.
- 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
of srcset
rules we define, and that is the sizes
attribute. The sizes
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 sizes
to "(min-width: 540px) 540w, 100vw"
we can prune
the 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
full-bleed (or 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,
such as srcset
rules for the following:
640w
for the iPhone SE. 320 logical pixels wide @ 2x750w
for the iPhone 6S. 375 logical pixels wide @ 2x1242w
for the iPhone 6S Plus. 414 logical pixels wide @ 3x
The finished product
Putting this all together, we get HTML that looks something like this:
<img srcset="https://assets.imgix.net/flower.jpg?w=50 50w,
https://assets.imgix.net/flower.jpg?w=100 100w,
https://assets.imgix.net/flower.jpg?w=150 150w,
...
https://assets.imgix.net/flower.jpg?w=540 540w,
https://assets.imgix.net/flower.jpg?w=640 640w
https://assets.imgix.net/flower.jpg?w=750 750w,
https://assets.imgix.net/flower.jpg?w=1080 1080w,
https://assets.imgix.net/flower.jpg?w=1242 1242w,
https://assets.imgix.net/flower.jpg?w=1620 1620w"
sizes="(min-width: 540px) 540w, 100vw"
src="https://assets.imgix.net/flower.jpg?w=540"
alt="A white flower"
/>
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.
Conclusion
Thus concludes this novel way of using srcset
with an exhaustive ruleset in the hopes of delivering a “more responsive” experience. The results are images that are pixel-perfect without needing to hang on JavaScript to begin loading images. This type of behavior can be seen in the newest version of imgix.js, although it does all the work on the client-side.
Special thanks to Matt Vanderpol and Jon Gold for providing feedback on early drafts of this.