by Marcelo Cortes4/28/2017
In the world of responsive web design one core, yet complicated, spec can net you substantial reductions in page size across the device spectrum. In this post I’ll demystify the complexity in the responsive images spec so you can use these powerful HTML attributes on your site. In part 2 you will learn how to build your own responsive image workflow, with a code demo that distills our responsive image stack into a single file. Also, we’ll dive into how we automate responsive images at scale processing millions of images at Webflow with AWS Lambda.
Let’s dive in!
<img> element has been around for a long time. Give it a
src attribute and you’re well on your way. The spec adds two new attributes which the browser uses to make an image responsive.
The new attributes are
srcset. To put it simply:
sizes tells the browser how big the
<img> will render, and
srcset gives the browser a list of image variants to choose from. The goal is to hint to the browser which variant in
srcset to start downloading as soon as possible.
The browser takes the
To see this in action, check out https://webflow.com/feature/responsive-images and open the network inspector, to see the browser loading the correct variants.
srcset is just a list of image variants. You can specify a pixel density next to each variant in the list like this
srcset=”http://variant-1.jpg 2x, http://variant-2.jpg 1.5x”. However this format only solves for hardware, serving better quality images on better quality displays, and does little for responsive design.
What you really want is to list variants by pixel width so that when your site is loaded on a mobile layout and rendered at 500px wide, or on a desktop layout at 750px wide it’ll only download the variant it needs to render that layout. The width-based format looks like this
srcset=”http://variant-1jpg 500w, http://variant-2.jpg 750w, http://variant-3.jpg 1000w, http://variant-4.jpg 1500w”. The
w here represents pixel width of the actual image file that the corresponding url points to.
Don’t worry, using the pixel width method buys you pixel density for free, your browser knows what it’s doing. Given the previous example on the mobile layout your browser would opt to download the 500px image on a 1x display and the 1000px wide image on a 2x display.
“What about the image’s height?” you say. To which I say, “The height is irrelevant, let’s not digress, don’t make me bring up vertical videos. Let’s move on.”
sizes is decidedly less simple. It has some things that look familiar, but should not be conflated, and quirks that are there for a reason; implemented after great debate amongst the standards bearers; but which even after understanding those reasons and agreeing with them quite frustrating to deal with and supernatural to witness. Unlike
srcset which lists image variants and never really needs to change once you’ve done that,
sizes may need to be updated any time you add a new element or tweak your css because it describes the dimensions of the rendered image. Sort of.
Our goal is to tell the browser how big the
<img> will render before it’s actually rendered. The browser trusts the
sizes attribute so implicitly that it will treat the width described therein as fact overlooking the actual size of the image variant after it’s been downloaded. This is called intrinsic size.
It’s not quite true to say that the
sizes attribute describes the size at which an image element will render. What it’s really doing is describing the size of the asset that would best fit the layout.
Imagine you have a container element that expands on hover, and an
<img> element inside which has
max-width: 100% so that it fills the container without growing larger than the actual width of the image file. With just a
src attribute we can predict how an image smaller than the hover state and one larger or equal to the width of the hover state will behave. With just a
src attribute the browser is determining the intrinsic size by measuring the actual image file.
sizes however the browser applies those same behaviors, but instead of measuring the variant after it’s downloaded it will determine the intrinsic size entirely from the
sizes attribute. It’s as if the browser creates a new image file in memory with the dimensions that match what’s defined in the
sizes attribute but filled in with the data from the actual variant it chooses to download from srcset. Providing variants larger than the intrinsic size will cause the data to shrink to fit and will look fine, but only providing smaller variants than what’s needed will force the browser to stretch the data to fill that in-memory image. This can be very noticeable so it’s important to get the
sizes attribute right.
You might expect in the case above that if you updated the
sizes attribute whenever you hover over the container that it would also update the intrinsic size. But the browser has already created that in-memory image and won’t create another one with a new intrinsic size unless the
srcset attribute changes. The item in srcset that was downloaded will cling to its original intrinsic size inferred during the initial parse.
The moral here is that once a published page is rendered, you shouldn’t attempt to modify the
sizes attribute. You should get it right the first time, and getting it right means telling the browser the size of the asset that would best fit the layout, which is to say the largest rendered size it could be at a given breakpoint. In our hover example above, it’s the width of the hover state.
sizes attribute looks like this:
sizes=”(max-width: 750px) 750px; 500px”. “Those look like media queries,” you likely just said to yourself. You are both right and wrong. They do look and kind of work like media queries, but they’re not media queries. That’s a media condition followed by something that is not css, but rather the hinted render width of the image within that media condition.
In our sizes attribute above with the browser at a screen width of 750px or smaller the
<img> will render as a 750px image (you can imagine this as a hero on mobile) and at any window width above that it’ll render at exactly 500px (you can imagine this as a profile picture). On desktop the browser will parse the html and it’ll say, “My window width is 1400px, I’m on a 2x display, and this
sizes attribute is telling me this
<img> will render as a 500px image in this media condition. 500px at 2x dpi means I should grab at least a 1000px wide variant and start downloading it asap.” If you don’t hear your browser saying this don’t lean in, I’m anthropomorphising.
A more complex
sizes attribute could look something like
(max-width: 479px) 96vw, (max-width: 767px) 500px, (max-width: 991px) 48vw, 50vw. You could make that even more complex by mixing in some
min-width conditions but why make life harder for yourself. The breakpoints in your media condition should simply correspond to the window widths at which your layout changes, but it’s only necessary to specify the ones on a particular
<img> where the width value for that
I should really stop referring to the window width at this point, what I mean to say is the viewport width. What you see here is a mix of px widths and viewport widths.
50vw is the same as saying
50% of the viewport width. It’s important to stress here that if you have an
<img /> inside a div, the browser is looking at your
srcset/sizes attributes after it’s parsed the html, but before it’s rendered the page or even parsed the css. The browser knows how big the viewport is, it does not have any idea how big that
div will render, so all
sizes fluid values must specify a percent of the viewport width to be meaningful rather than a percent of their parent or relative ancestor.
For some layouts, for example where an
<img> element has multiple ancestors each with different margins, you can also use
calc values. In most real-world circumstances being that precise will only save you a few bytes while substantially increasing complexity. It’s important to remember that when simplifying those complex
sizes cases it’s better to overestimate than underestimate.
Imagine a similar case on a 1x display, where
sizes specifies the image is 1000px wide. In the event that you didn’t specify a 1000px wide variant in
srcset, but did specify a larger one it’s fine, your browser will grab the larger one, say a 1200px image variant, and it’ll set its intrinsic size to 1000px wide. If however you only have smaller than 1000px wide variants, say the largest you have is 800px wide, your browser will download that one (the largest available) and set its intrinsic size to 1000px. Your browser fully intends to stretch that thing unless you override it with css, but now we’re in this convoluted space. You took a wrong turn to get here.
Don’t expect to just stick your master image in that good old
src attribute and be saved in this scenario either. Once a browser sees that you’ve used
sizes on an
<img> it’ll pretend it doesn’t see
src. Put that master image at the end of your
srcset list if you want to it to be an option for newer browsers. Just make sure you keep it in the
src attribute as well for older browsers which don’t see
Downloading a larger variant than is needed will waste a few bytes of bandwidth and could be combated by having variants that match your layout if your layout happens to be rigid enough within each media query to support that, but in cases where the
sizes attributes describes a fluid size within a media query eg:
50vw the solution is to have variants that are small in filesize and at sensible widths to minimize wasted bytes. Typically the filesize reduction from a master image to any variant is so large that the difference in filesize between two variants is negligible in real world examples. So take a deep breath and don’t over think it.
When testing responsive images it’s important to note that your browser has a strong preference for cache. Since the point of responsive images is to download as few bytes as possible, the browser doesn’t see a need to download smaller variants of an image than the one it has cached.
Assume you have an
<img> element where the sizes attribute is defined in such a way that each larger media condition requires a larger variant — you can do this simply with
sizes=”100%”. If you were to open your browser at 300px wide and slowly resize it to 2500px wide, as you passed each variant width the browser would realize it needs each larger variant and you’d see the intrinsic size of the previous variant get stuck until the larger variant you now need is downloaded and rendered. If you check the network tab you’d see a request for each of the variants you needed as you did this. However if you start with a fresh cache at 2500px wide viewport and gradually resize it down to 300px wide you would only see a single request for the largest variant it needed when it rendered.
Similarly between refreshing and testing different
sizes attribute values you should reset your cache, because the effects of intrinsic size will be less apparent if you’re working with a cached large variant rather than the smaller variant a user would get loading your page at a given viewport size for the first time with an empty cache. Ie: The variant used to build that in-memory intrinsic image will be different to someone with a fresh cache.
So the question you may be asking yourself is why go through all this. Obviously smaller page sizes and load times will lower your hosting bill and improve customer experience. And sending a 5000px wide image to a smartphone with a bad connection when it only needs something 1/10th the size is not best for anyone. As a medium we should be striving to reduce barriers to content where we can. But there are other approaches to responsive images that are simpler to implement.
With a little work and thought you can shape experience to be smoother and more efficient. You could have ‘make images responsive’ as a final checkbox before you launch a site to limit the work required, but ideally you want to work with the responsive nature and all the quirks of intrinsic size and not relegate them to an afterthought. Like molding clay or painting on a canvas you want to feel the medium you’re working with as you create. In Part 2 I’ll dive into how we automate responsive images and provide that real-time feedback at Webflow.
Interested in working on the next generation web publishing platform? Webflow is hiring!
Marcelo Cortes is a co-founder and the CTO of Faire, an online wholesale marketplace connecting mostly small brands to independent, local retailers.