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 Part 1 "How to use Responsive Images" we learned about a general common sense approach to the srcset and sizes attributes and some quirky behavior you’re likely to encounter while working with them.
In this post I’ll dive into how we do responsive images at scale at Webflow, and walk through a runable code demo that distills our automated responsive image processing stack into a single file to get you started automating your own workflow.
You certainly don’t need to in order to follow along with this article, but if you’re so inclined now might be a good time to switch over to the demo code, you can follow along with the in-code comments and use this part of the post as an augmented guide for deeper explanation.
Let’s dive in!
How We Handle Automation
Under the hood Webflow is actually several advanced apps tightly knit together. We’re going to zoom in on the Designer with a specificity that gives us a good overview of what’s happening but without too many implementation details that won’t be very meaningful out of context.
The Designer has two jobs when it comes to responsive images. First, whenever you upload a compatible master image we generate a set of responsive variants — smaller versions of your master image. Those variants are used to populate the srcset attribute whenever you connect that master image to an inline image element <img>. Secondly whenever you change the DOM or CSS we measure the <img>s on your page at different breakpoints in the browser to calculate the value for the sizes attribute based on how they actually render.
Let’s take a high level look at how we handle those tasks in Webflow, and then we’ll walk through a working demo to get our hands dirty.
Generating Responsive Image Variants
When you upload a compatible image to your site, it travels through a pipeline where that asset is resized down to a set of predefined widths, and all those variants are saved and compressed along with the master image for later use. This is a one-time process for that image. When connecting that image asset to an <img> html element those pre-generated variants will be used to populate the srcset attribute, in the Designer you can connect the same image asset to multiple <img> elements and we just link to the same variants when you do.
At Webflow we use AWS Lambda to generate responsive variants. Lambda gives us infinite scale to handle the millions of images we process every month. We also generate variants lazily so that you can start working with the master image while it’s still uploading. Later on (usually a few seconds later on) when the variants are ready they’ll be applied as needed.
You may be wondering how we know what size variants to generate at upload time since all we have to go on at that point are the master image’s dimensions.
Deciding on variant widths
We settled on a set of pre-determined variant widths by looking at the default media queries already used in the Designer and processing image metadata of millions of images to figure out what the optimal variant sizes would be in our use case. Which is to say, what would work best for most use cases across Webflow. In the future we may enable customization here for advanced users, but to start we wanted responsive images to “just work” for most designs in most cases without having to think about it, your mileage may vary.
We arrived at the following constant set: 500px, 800px, 1080px, 1600px, 2000px, 2600px, and 3200px. The main features of the set are that the distance between variants increases as the dimensions increase, and every variant has a corresponding 2x variant. The quirks you see, like having a 1080px where you’d expect 1000px, are there due to quirks in our dataset.
You can read more about the advantages and disadvantages of trying to be too precise with matching your design to your variant widths in Part 1.
Measuring Image Elements
On your published site the sizes attribute for each image element is used to hint to the browser (before it downloads any images) how big that image element will render so that the browser can choose the most efficient variant to download. The size that an image element will render on your live site can be affected by the natural size of the image asset as well as the structure and css of the page.
In order to compute these values the Designer automatically renders your page — at every breakpoint by literally rendering and resizing the page, and taking measurements of each image behind the scenes as you work in a hidden worker canvas. Part of the magic of the Designer running in the browser. It then compiles those measurements and optimizes them into a concise value for the sizes attribute. To get around some of the effects of intrinsic size detailed in Part 1 once we calculate a new sizes value we also flash the srcset/sizes attributes to force the browser to re-evaluate the attributes so that you can get a feel for how they’ll affect your published site as you work and make changes in the Designer. One of our values at Webflow is to keep designers working in the true medium of the web.
Working Through the Code
The demo distills the core parts of our stack at Webflow into a single js file. It’s designed to get you up and running for simple use cases to drive home the concepts, and get your hands dirty following the hints on where and how to improve it to handle some edge cases.
Generating Image Variants (Resizing and Compression)
The first step is to generate image variants. In the demo we’re using libvips to do the image resizing. If you’re familiar with ImageMagick and GraphicsMagick, libvips has less functionality but is about 10x faster. There’s an array of different tools for further optimizing your responsive variants. Pngquant, jpeg-recompress, and Guetzli are great examples of lossy compression steps. They aim to throw out bits of data that the human eye would ignore anyway to minimize file size. There are also settings and libraries that aim to be lossless by simply reorganizing the data or describing the repeating patterns in ways that result in smaller file size.
Typically when resizing images you resize the rendered bitmap and then apply some compression to it to optimize it for the web. It’s possible to run into situations where a smaller variant (eg: 500w) ends up with a larger file size than one of its larger siblings (eg: 800w). Compression is rarely a one-size-fits-all endeavour. Even the ‘same’ image at different sizes is really a different set of 1s and 0s that a given compression algorithm may struggle to find the same patterns in. You can apply multiple compression flows known to work with different types of images and only keep the best one, or just throw out variants that don’t end up with smaller file size than one of their larger siblings.
At Webflow when a particular variant doesn’t compress well compared to its sibling variants we mark it so that the inefficient variant doesn’t get served on your published site, and keep it with your other variants so we can iterate on our compression later and benchmark against it.
Measuring and Calculating Rendered <img> size
The next part of the demo focuses on generating the sizes attribute. The goal is to render the html in a real browser and take measurements of how the <img> elements actually render. While you could try to infer how big an image will render just by looking at the code ( the html and css on a site), you’ll quickly find a plethora of edge cases where css properties and layout of other elements on the page affect how large a given image renders at. It’s rarely straightforward and soon after you dig your way into the rabbit hole of inferring all those interactions you’ll realize you’re just re-implementing a browser’s rendering engine and it would be faster to just let it render and take measurements after it does.
The demo only has a single window width within each media query where it takes measurements. Taking a look at the generated index.html in Chrome you may notice that there is some stretching when it comes to variants that are actually sized by either their own or their parent’s percent-based width or margins. One way to mitigate that would be to take multiple measurements within a media query to see how an `<img>` element grows relative to the viewport width, and use those measurements to infer either a px, vw, or calc value for the sizes attribute under that media condition.
To demonstrate this take the case of a 2000px wide image asset in an <img> tag that’s set to max-width: 100% on an otherwise empty page. If you only measure it at 1500px and 2000px viewport widths your measurement could be interpreted as 100vw. However if you measured it at 1500px, 2000px, and 2500px you would infer that up to a viewport width of 2000px the image behaves like it’s 100vw (growing with the size of the window), but once the viewport reaches the natural size of the image the <img> element doesn’t grow beyond that 2000px wide; this would be specified as sizes=”(max-width: 2000px) 100vw, 2000px”.
This can get quite tricky in more advanced cases. For example imagine multiple ancestors have % and px based margins. When simplifying your layout is not an option you can lessen the effect of these sizes deviations from truth by deciding on smart variant widths to generate and upping the quality of your variants. You’ll find in general images that contain text or sharp edges are less tolerant to compression than images that contain busy scenery.
Applying Responsive Attributes
Finally the demo compiles the measurements into sizes attributes, and variants into srcset attributes, applies them to the html and generates a modified index.html page.
The sizes attribute should always be specified if srcset is specified. While the default sizes attribute value is 100vw choosing to leave this attribute blank will not affect the intrinsic size of the asset in the same way as if it were specified. This will lead to unpredictable behavior and is not valid html for that reason.
Similarly the last value of your sizes attribute should be without a media condition as a fallback. In our use case it’s any viewport size above and beyond what we expect in reality. In the demo and in the Designer we take an additional measurement at a 10,000px viewport width which is intended to be the largest screen size we’d expect a site visitor to view the page on.
In the Designer, we have that hidden worker canvas whose sole job is taking measurements of the rendered page to calculate the sizes attributes. In order to remove complexity of intrinsic size and measure the images as they would render without responsive images, that worker canvas never uses srcset or sizes attributes and uses special placeholder images that match the dimensions of the actual asset being used. Once we take measurements and come up with new sizes value, we remove both srcset and sizes attributes from the regular canvas and then re-apply the new values. Even though the srcset value never changes for the same image asset, we still remove and re-add it along with sizes because then only time your browser reads the sizes attribute is to create the in-memory placeholder for the variant it ends up downloading.
We’re optimizing both our image compression stack to make better looking variants with smaller file sizes for the wide range of image types we see, and our in-browser measurements to handle the advanced layouts that are possible with Webflow. We try to keep it as hands-off and magical as possible so that designers don’t need to think about it, but enable designers to turn it off for specific <img> elements when they want to do something outside the box that our algorithms don’t yet address.
Ultimately we believe tedious busy work like generating variants and measuring images should be abstracted away to computers so that designers can stay focused and immersed in the design process.
Interested in working on the next generation web publishing platform? Webflow is hiring!