Cloudinary Blog

Responsive images with 'srcset', 'sizes' and Cloudinary

Cloud-hosted responsive images with srcset and sizes

This is a guest post by Eric Portis – a proud member of (and newsletter-writer for) the Responsive Issues Community Group. The RICG formulated, championed, and standardized the new HTML features presented in the article.

The "responsive images" problem

Five years ago, Ethan Marcotte coined the term “responsive web design” and gave it three defining ingredients: fluid grids, flexible media, and media queries.

That second ingredient, “flexible media” turned out to be a bit of a bugbear.

You see, most of the media on the web is bitmap images. Heck, measured by bytes, most of the web is bitmap images. And bitmap images are fixed. They have a fixed height and a fixed width, and while it’s possible to make them visually grow and shrink using width and max-width CSS declarations, forcing users on small, low-resolution displays to load enormous images which have been sized to look good on high-resolution displays is a performance disaster. Every time we send a user more pixels than their device can use, we’re asking them to load data which they’ll end up throwing away. We’re sending them waste.

How much waste? Tim Kadlec has estimated that — for users on low-res mobile devices — 72% of image bytes served up by responsive pages are wasted. Responsive pages are sending small-screened users nearly four times as much data as they need.

New markup is here to save the day. We can’t include a single image resource that’ll work for everybody, but by using new responsive image features, we can stuff multiple resources into a single <img> and mark them up in such a way that browsers will load the most appropriate resource out of the set. We can mark up image elements which have the ability to adapt.

Let’s dive in with an example: a news-y article page with a single, large-ish image up top:

https://ericportis.com/etc/cloudinary/castro-on-the-line/

Responsification with srcset and sizes

srcset

How can we make this image responsive? The first tool in our belt is an <img> attribute named srcset. srcset allows us to stuff multiple resources into a single <img>, via a comma separated list:

Copy to clipboard
    srcset="large.jpg 1024w, medium.jpg 512w, small.jpg 256w"

We label each resource with a “width descriptor” — those numbers with 'w's after them, above. These let the browser know how wide each resource is. If a resource is 1024 pixels wide, stick a 1024w after it in srcset.

President Barack Obama talks on the phone with Cuba President Raúl Castro in the Oval Office.

Browsers will load the resource that best matches the current context. So, if the image is 512px wide on the layout, users on standard screens will probably get medium.jpg, and users on HiDPI display will probably get large.jpg.

Why “probably?” Ultimately, browsers decide which users get which resources. And we, as authors, can’t necessarily predict which sorts of users will get what. This is a good thing! Browsers know a lot more about their context than we do. They have a wealth of information about each user’s device, connection, and preferences and the pick-as-you-please philosophy of srcset lets them leverage it.

So, let’s add a srcset attribute to an <img>:

Copy to clipboard
    <img srcset="small.jpg 256w,
                 medium.jpg 512w,
                 large.jpg 1024w"
         src="medium.jpg"
         alt="It’s responsive!" />

Unfortunately, that won’t quite cut it. When browsers encounter our srcset, they’re missing a crucial piece of information: the <img>’s layout width. In order to know an image’s display dimensions, browsers have to load and parse all of its page’s CSS – something which usually happens long after srcset-parsing. To cope, we need to give the browser an estimate of the layout width of the image right in our markup, using a new attribute: sizes.

sizes

sizes takes CSS lengths. If our <img> had a fixed width, we could simply stick that width in a sizes and call it a day. For instance – if it was always 640px across on the layout, our sizes would look like this:

Copy to clipboard
    sizes="640px"

Our example image is a little more complicated. Below its max width of 30em, it occupies the full width of the viewport: 100vw. Above that max-width, it will always be 30em wide (learn about 'em' and 'vw' CSS units)

So our image has two possible sizes. Luckily, just like srcset, sizes lets us supply multiple values in a comma-separated list. And it lets us tell the browser which lengths to use, when, using media queries. So to give the browser a complete picture of our <img>’s dynamic layout width, we can mark it up with the following:

Copy to clipboard
    sizes="(min-width: 30em) 30em, 100vw"

That says, if the viewport is at least 30em wide, this image will be 30em wide[1]. Otherwise, the image will be 100vw wide.

Putting all of the pieces together, we arrive at the following markup:

Copy to clipboard
    <img srcset="small.jpg 256w,
                 medium.jpg 512w,
                 large.jpg 1024w"
         sizes="(max-width: 30em) 30em, 100vw"
         src="medium.jpg"
         alt="It’s responsive!" />

This gives us a fluid, adaptable image, which will look crisp on large, hi-res displays and minimize the number of wasted image bytes sent to users on low-res devices[2].

But! Now we, as authors, have a new problem: creating and managing three resources instead of just one.

Rendering small, medium, and large versions for our single-image example is a minor inconvenience. But when multiplied by a whole page’s worth of images, this extra work becomes a real burden. Asking poor, mistake-prone humans to render multiple versions of images at specific sizes very carefully and entirely by hand is both cruel and foolish. We need to automate.

Cloudification

Using Cloudinary, we can upload a single, canonical, high-resolution image to the cloud and deliver it in as many different sizes as we want via URL parameters (or Cloudinary’s various SDKs). Controlling our images’ technical characteristics via text and code has enormous benefits:

  • Image characteristics are easy to author, see, and alter, right from the markup
  • We can encode these characteristics directly in markup templates
  • We can track changes to these characteristics using version control

Using Cloudinary, adding a new image size (or changing an old one) on a site with thousands of images is as easy as changing a few lines in a template file. And content authors only have to worry about creating and uploading the highest-resolution image that they have; the nitty-gritty details of how that image will be variously compressed, sized, and served to users are handled automatically.

So let’s re-visit our example. And this time let’s not simply assume that we have multiple down-sized versions of our image readily at hand — let’s generate those resources with Cloudinary.

After creating a free Cloudinary account I uploaded the full-res (1.2MB!) version of the example image to my Media Library and gave it an easy-to-remember name: on_the_phone. Once uploaded, I can access it using the following URL.

Ruby:
Copy to clipboard
cl_image_tag("on_the_phone.jpg")
PHP v1:
Copy to clipboard
cl_image_tag("on_the_phone.jpg")
PHP v2:
Copy to clipboard
(new ImageTag('on_the_phone.jpg'));
Python:
Copy to clipboard
CloudinaryImage("on_the_phone.jpg").image()
Node.js:
Copy to clipboard
cloudinary.image("on_the_phone.jpg")
Java:
Copy to clipboard
cloudinary.url().imageTag("on_the_phone.jpg");
JS:
Copy to clipboard
cloudinary.imageTag('on_the_phone.jpg').toHtml();
jQuery:
Copy to clipboard
$.cloudinary.image("on_the_phone.jpg")
React:
Copy to clipboard
<Image publicId="on_the_phone.jpg" >

</Image>
Vue.js:
Copy to clipboard
<cld-image publicId="on_the_phone.jpg" >

</cld-image>
Angular:
Copy to clipboard
<cl-image public-id="on_the_phone.jpg" >

</cl-image>
.NET:
Copy to clipboard
cloudinary.Api.UrlImgUp.BuildImageTag("on_the_phone.jpg")
Android:
Copy to clipboard
MediaManager.get().url().generate("on_the_phone.jpg");
iOS:
Copy to clipboard
imageView.cldSetImage(cloudinary.createUrl().generate("on_the_phone.jpg")!, cloudinary: cloudinary)
Original hi-res image

Let’s break this URL down:

  • res.cloudinary.com/: the base Cloudinary URL
  • eeeps: my Cloudinary cloud name
  • image/upload/: the path to images uploaded to my media library
  • on-the-phone the name of the image we’d like to access
  • .jpg: the desired format. We could just as easily request a .png or .gif here; even though the original, uploaded image is a JPEG, Cloudinary will do the conversion on the fly.

All of the resizing action is going to happen between the upload/ and the image name. This is where image transformations go — the bits that are going to let us control exactly how our image is compressed, shaped, and sized for delivery[3].

The first thing that we’re going to want to do to this image is scale it down. To do that, we’ll use Cloudinary’s width parameter. Stick a w_ and a number into our URL, and Cloudinary will give us our image scaled to that many pixels across.

Ruby:
Copy to clipboard
cl_image_tag("on_the_phone.jpg", :width=>512, :crop=>"scale")
PHP v1:
Copy to clipboard
cl_image_tag("on_the_phone.jpg", array("width"=>512, "crop"=>"scale"))
PHP v2:
Copy to clipboard
(new ImageTag('on_the_phone.jpg'))
  ->resize(Resize::scale()->width(512));
Python:
Copy to clipboard
CloudinaryImage("on_the_phone.jpg").image(width=512, crop="scale")
Node.js:
Copy to clipboard
cloudinary.image("on_the_phone.jpg", {width: 512, crop: "scale"})
Java:
Copy to clipboard
cloudinary.url().transformation(new Transformation().width(512).crop("scale")).imageTag("on_the_phone.jpg");
JS:
Copy to clipboard
cloudinary.imageTag('on_the_phone.jpg', {width: 512, crop: "scale"}).toHtml();
jQuery:
Copy to clipboard
$.cloudinary.image("on_the_phone.jpg", {width: 512, crop: "scale"})
React:
Copy to clipboard
<Image publicId="on_the_phone.jpg" >
  <Transformation width="512" crop="scale" />
</Image>
Vue.js:
Copy to clipboard
<cld-image publicId="on_the_phone.jpg" >
  <cld-transformation width="512" crop="scale" />
</cld-image>
Angular:
Copy to clipboard
<cl-image public-id="on_the_phone.jpg" >
  <cl-transformation width="512" crop="scale">
  </cl-transformation>
</cl-image>
.NET:
Copy to clipboard
cloudinary.Api.UrlImgUp.Transform(new Transformation().Width(512).Crop("scale")).BuildImageTag("on_the_phone.jpg")
Android:
Copy to clipboard
MediaManager.get().url().transformation(new Transformation().width(512).crop("scale")).generate("on_the_phone.jpg");
iOS:
Copy to clipboard
imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation().setWidth(512).setCrop("scale")).generate("on_the_phone.jpg")!, cloudinary: cloudinary)
Resized image

This scaled-down, 512-pixel-wide image weighs in at 86KB. How could we slim it down even further?

Cloudinary’s default compression settings are fairly conservative, resulting in heavy images of high quality. We can choose our own tradeoff – sacrificing a little quality for lighter images – using the quality parameter. The quality parameter takes a number between 0 and 100; after a little trial and error, I settled on 70.

Ruby:
Copy to clipboard
cl_image_tag("on_the_phone.jpg", :quality=>70, :width=>512, :crop=>"scale")
PHP v1:
Copy to clipboard
cl_image_tag("on_the_phone.jpg", array("quality"=>70, "width"=>512, "crop"=>"scale"))
PHP v2:
Copy to clipboard
(new ImageTag('on_the_phone.jpg'))
  ->resize(Resize::scale()->width(512))
  ->delivery(Delivery::quality(70));
Python:
Copy to clipboard
CloudinaryImage("on_the_phone.jpg").image(quality=70, width=512, crop="scale")
Node.js:
Copy to clipboard
cloudinary.image("on_the_phone.jpg", {quality: 70, width: 512, crop: "scale"})
Java:
Copy to clipboard
cloudinary.url().transformation(new Transformation().quality(70).width(512).crop("scale")).imageTag("on_the_phone.jpg");
JS:
Copy to clipboard
cloudinary.imageTag('on_the_phone.jpg', {quality: 70, width: 512, crop: "scale"}).toHtml();
jQuery:
Copy to clipboard
$.cloudinary.image("on_the_phone.jpg", {quality: 70, width: 512, crop: "scale"})
React:
Copy to clipboard
<Image publicId="on_the_phone.jpg" >
  <Transformation quality="70" width="512" crop="scale" />
</Image>
Vue.js:
Copy to clipboard
<cld-image publicId="on_the_phone.jpg" >
  <cld-transformation quality="70" width="512" crop="scale" />
</cld-image>
Angular:
Copy to clipboard
<cl-image public-id="on_the_phone.jpg" >
  <cl-transformation quality="70" width="512" crop="scale">
  </cl-transformation>
</cl-image>
.NET:
Copy to clipboard
cloudinary.Api.UrlImgUp.Transform(new Transformation().Quality(70).Width(512).Crop("scale")).BuildImageTag("on_the_phone.jpg")
Android:
Copy to clipboard
MediaManager.get().url().transformation(new Transformation().quality(70).width(512).crop("scale")).generate("on_the_phone.jpg");
iOS:
Copy to clipboard
imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation().setQuality(70).setWidth(512).setCrop("scale")).generate("on_the_phone.jpg")!, cloudinary: cloudinary)
Resized image of 70% quality

This results in an image that’s 46KB — half the size of the unq_’d version! What else could we do to cut weight? What if tried a different format entirely, like Google’s relatively-new WebP format?

Ruby:
Copy to clipboard
cl_image_tag("on_the_phone.webp", :quality=>70, :width=>512, :crop=>"scale")
PHP v1:
Copy to clipboard
cl_image_tag("on_the_phone.webp", array("quality"=>70, "width"=>512, "crop"=>"scale"))
PHP v2:
Copy to clipboard
(new ImageTag('on_the_phone.webp'))
  ->resize(Resize::scale()->width(512))
  ->delivery(Delivery::quality(70));
Python:
Copy to clipboard
CloudinaryImage("on_the_phone.webp").image(quality=70, width=512, crop="scale")
Node.js:
Copy to clipboard
cloudinary.image("on_the_phone.webp", {quality: 70, width: 512, crop: "scale"})
Java:
Copy to clipboard
cloudinary.url().transformation(new Transformation().quality(70).width(512).crop("scale")).imageTag("on_the_phone.webp");
JS:
Copy to clipboard
cloudinary.imageTag('on_the_phone.webp', {quality: 70, width: 512, crop: "scale"}).toHtml();
jQuery:
Copy to clipboard
$.cloudinary.image("on_the_phone.webp", {quality: 70, width: 512, crop: "scale"})
React:
Copy to clipboard
<Image publicId="on_the_phone.webp" >
  <Transformation quality="70" width="512" crop="scale" />
</Image>
Vue.js:
Copy to clipboard
<cld-image publicId="on_the_phone.webp" >
  <cld-transformation quality="70" width="512" crop="scale" />
</cld-image>
Angular:
Copy to clipboard
<cl-image public-id="on_the_phone.webp" >
  <cl-transformation quality="70" width="512" crop="scale">
  </cl-transformation>
</cl-image>
.NET:
Copy to clipboard
cloudinary.Api.UrlImgUp.Transform(new Transformation().Quality(70).Width(512).Crop("scale")).BuildImageTag("on_the_phone.webp")
Android:
Copy to clipboard
MediaManager.get().url().transformation(new Transformation().quality(70).width(512).crop("scale")).generate("on_the_phone.webp");
iOS:
Copy to clipboard
imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation().setQuality(70).setWidth(512).setCrop("scale")).generate("on_the_phone.webp")!, cloudinary: cloudinary)

That cuts the size of the image by more than a third again, bringing it down to 36KB. But Here's what that WebP looks like in anything but Chrome:

A broken image

That clearly won’t do. How can we send new, advanced image formats to browsers that support them, without showing broken image icons to browsers that don’t?

There’s a whole new markup pattern that’s been spec’d specifically to address this problem, but using Cloudinary, we don’t need it. Just stick an f_auto parameter into an image URL, and Cloudinary will perform some server-side content-negotiation magic to ensure that every browser gets the latest and greatest format that it supports:

Ruby:
Copy to clipboard
cl_image_tag("on_the_phone.jpg", :quality=>70, :width=>512, :fetch_format=>:auto, :crop=>"scale")
PHP v1:
Copy to clipboard
cl_image_tag("on_the_phone.jpg", array("quality"=>70, "width"=>512, "fetch_format"=>"auto", "crop"=>"scale"))
PHP v2:
Copy to clipboard
(new ImageTag('on_the_phone'))
  ->resize(Resize::scale()->width(512))
  ->delivery(Delivery::format(Format::auto()))
  ->delivery(Delivery::quality(70));
Python:
Copy to clipboard
CloudinaryImage("on_the_phone.jpg").image(quality=70, width=512, fetch_format="auto", crop="scale")
Node.js:
Copy to clipboard
cloudinary.image("on_the_phone.jpg", {quality: 70, width: 512, fetch_format: "auto", crop: "scale"})
Java:
Copy to clipboard
cloudinary.url().transformation(new Transformation().quality(70).width(512).fetchFormat("auto").crop("scale")).imageTag("on_the_phone.jpg");
JS:
Copy to clipboard
cloudinary.imageTag('on_the_phone.jpg', {quality: 70, width: 512, fetchFormat: "auto", crop: "scale"}).toHtml();
jQuery:
Copy to clipboard
$.cloudinary.image("on_the_phone.jpg", {quality: 70, width: 512, fetch_format: "auto", crop: "scale"})
React:
Copy to clipboard
<Image publicId="on_the_phone.jpg" >
  <Transformation quality="70" width="512" fetchFormat="auto" crop="scale" />
</Image>
Vue.js:
Copy to clipboard
<cld-image publicId="on_the_phone.jpg" >
  <cld-transformation quality="70" width="512" fetchFormat="auto" crop="scale" />
</cld-image>
Angular:
Copy to clipboard
<cl-image public-id="on_the_phone.jpg" >
  <cl-transformation quality="70" width="512" fetch-format="auto" crop="scale">
  </cl-transformation>
</cl-image>
.NET:
Copy to clipboard
cloudinary.Api.UrlImgUp.Transform(new Transformation().Quality(70).Width(512).FetchFormat("auto").Crop("scale")).BuildImageTag("on_the_phone.jpg")
Android:
Copy to clipboard
MediaManager.get().url().transformation(new Transformation().quality(70).width(512).fetchFormat("auto").crop("scale")).generate("on_the_phone.jpg");
iOS:
Copy to clipboard
imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation().setQuality(70).setWidth(512).setFetchFormat("auto").setCrop("scale")).generate("on_the_phone.jpg")!, cloudinary: cloudinary)
Automatic WebP delivery

When accessing this URL, Chrome will get a WebP, Edge and IE 9+ will get a JPEG-XR, and everybody else will get a JPEG.

Okay – now that we know how to use Cloudinary’s image transformations to generate resized (and optimized) versions of our original, let’s stick some Cloudinary URLs into our responsive image markup:

Copy to clipboard
    <img
        sizes="(min-width: 30em) 28em, 100vw"
        srcset="https://res.cloudinary.com/eeeps/image/upload/f_auto,q_70,w_256/on_the_phone.jpg 256w,
                https://res.cloudinary.com/eeeps/image/upload/f_auto,q_70,w_512/on_the_phone.jpg 512w,
                https://res.cloudinary.com/eeeps/image/upload/f_auto,q_70,w_768/on_the_phone.jpg 768w,
                https://res.cloudinary.com/eeeps/image/upload/f_auto,q_70,w_1024/on_the_phone.jpg 1024w,
                https://res.cloudinary.com/eeeps/image/upload/f_auto,q_70,w_1280/on_the_phone.jpg 1280w"
        src="https://res.cloudinary.com/eeeps/image/upload/f_auto,q_70,w_512/on_the_phone.jpg"
        alt />

That’s the fully-responsive <img> as it appears on our example page.

Screenshot of the example page

This <img> will adapt right along with our responsive layout, serving a well-optimized version of our resource to users with differing viewport sizes, device resolutions, and browser-format-support capabilities.

Even better, with this markup, we bring the number of image resources that we have to worry about creating and managing back down to one: the high-res original we upload to Cloudinary. And if we ever want to tweak how we’re actually delivering that image to users, it’s as simple as changing a few characters in our HTML.

Responsive images are here, and cloud-based image hosting makes them easy.

Thus concludes part one of a two-part series. Next time, I’ll show you how to pair <picture> with Cloudinary’s cropping transforms to add another dimension of adaptability to our images: art direction. Stay tuned!


[1]: Particularly sharp readers may notice a slight problem with this: em are relative, and 30em in one context might not equal 30em in another. Within the context of sizes, an em is always equal to the default font size (usually 16px). But within the context of a CSS declaration, the size of an em is dependent on the font-size of the selected element. And indeed, our page’s viewport-sized typography means that the two 30ems will almost never be precisely the same. But you know what? They’re close enough! A precise sizes value would make our markup harder to read, understand, and maintain – and would only effect which source was chosen in a tiny number of cases. In horseshoes, hand grenades, and sizes: close counts.

[2]: How do browsers actually use srcset and sizes to pick a source? They divide the resource widths (the ws), by the current layout width (figured out via sizes) to calculate each resource’s “image density”. So, let’s say our sizes evaluates to a layout width of 512px, and we’re working with a srcset="small.jpg 256w, medium.jpg 512w, large.jpg 1024w". The image densities would work out to:

small.jpg: 256w resource width ÷ 512px layout width = 0.5x image density medium.jpg: 512w resource width ÷ 512px layout width = 1x image density large.jpg: 1024w resource width ÷ 512px layout width = 2x image density

Browsers make their decision based on these image densities. So users on 1x devices will probably get medium.jpg, users on Hi-DPI displays will most likely load large.jpg, and perhaps users on slow connections, or who’ve set some sort of preference stating that they suffer from poor eyesight will get small.jpg. But again, there’s no way for us, as authors, to know for certain which sorts of users will get what. In the browser we trust.

[3]: Cloudinary’s SDK frameworks give you language-specific tools to build these URLs indirectly. Check out the tabs in the examples to see how achieve each transformation using your framework of choice.

Recent Blog Posts

Generate Waveform Images from Audio with Cloudinary

This is a reposting of an article written by David Walsh. Check out his blog HERE!
I've been working a lot with visualizations lately, which is a far cry from your normal webpage element interaction coding; you need advanced geometry knowledge, render and performance knowledge, and much more. It's been a great learning experience but it can be challenging and isn't always an interest of all web developers. That's why we use apps and services specializing in complex tasks like Cloudinary: we need it done quickly and by a tool written by an expert.

Read more
Make All Images on Your Website Responsive in 3 Easy Steps

Images are crucial to website performance, but most still don't implement responsive images. It’s not just about fitting an image on the screen, but also making the the image size relatively rational to the device. The srcset and sizes options, which are your best hit are hard to implement. Cloudinary provides an easier way, which we will discuss in this article.

Read more

The Future of Audio and Video on the Web

By Prosper Otemuyiwa
The Future of Audio and Video on the Web

Web sites and platforms are becoming increasingly media-rich. Today, approximately 62 percent of internet traffic is made up of images, with audio and video constituting a growing percentage of the bytes.

Read more

Embed Images in Email Campaigns at Scale

By Sourav Kundu
Embed Images in Email Campaigns at Scale

tl;dr

Cloudinary is a powerful image hosting solution for email marketing campaigns of any size. With features such as advanced image optimization and on-the-fly image transformation, backed by a global CDN, Cloudinary provides the base for a seamless user experience in your email campaigns leading to increased conversion and performance.

Read more
Build the Back-End For Your Own Instagram-style App with Cloudinary

Github Repo

Managing media files (processing, storage and manipulation) is one of the biggest challenges we encounter as practical developers. These challenges include:

A great service called Cloudinary can help us overcome many of these challenges. Together with Cloudinary, let's work on solutions to these challenges and hopefully have a simpler mental model towards media management.

Read more

Build A Miniflix in 10 Minutes

By Prosper Otemuyiwa
Build A Miniflix in 10 Minutes

Developers are constantly faced with challenges of building complex products every single day. And there are constraints on the time needed to build out the features of these products.

Engineering and Product managers want to beat deadlines for projects daily. CEOs want to roll out new products as fast as possible. Entrepreneurs need their MVPs like yesterday. With this in mind, what should developers do?

Read more