The blog of , a Ruby on Rails development team
Known from Rails LTS, makandropedia and Nanomize

Using responsive images

Back in the web's youth, you'd embed an image with <img src="some/file.jpg" />. Everyone who read your site would download and see that image. After all, it was only a small file a few pixels high and wide.

Times have changed. Web pages contain huge images. People visit web pages from various devices with different screen sizes and resolutions. Moreover, hi-DPI screens have more pixels per area, requiring higher-resolution images.

Web pages need to adapt. But how?

Naive approach

The easiest means developer-wise would be to simply provide images with double (or triple) the traditional resolution.

Unfortunately, this has the drawback of delivering 4x (or 9x) as large image files to everyone. Even people with a 3.5 inch display would need to load the full 4K landing page image over their prepaid mobile connection.

That's bad.

Flexible approach (the future)

What if we gave each client exactly the image size it needed? It would mean more work for the developer, but combine the best user experience with the best possible load time for the client.

That's called a "responsive image". Like a responsive web page, it sort of "adapts" to the client's screen size. Unlike a responsive web page, there is some effort required to achieve this.

Using responsive images is a trade-off between storage and complexity on the server side, and perceived performance and quality on the client side. Offering 100 versions for an image will give each device the optimum image file as quick as possible, but consume lots of space on the server and make rendering and maintenance a nightmare.

Hence, it is about finding the sweet spots of valuable image sizes. Nowadays, browsers are quite good in interpolating images, so you do not actually need to target all resolutions - which, by the way, is impossible. You could never target all mobile device resolutions.

Let's get to the meat. Here's the process (inspired by Matt Wilcox). Do this for each image location in your application (e.g. header banner, image thumbs, gallery, etc.):

  • Determine required image widths
  • Generate them
  • Render them

However, it is not totally as simple as this, so we'll talk about the process in detail.

1. Finish the design

You'll save yourself wasted time when you only start optimizing image performance after all conditions have settled, including responsive design! Remember, premature optimization is the root of all evil.

2. Pick a standard version

You should always have a standard image that's just as large as it needs to be for a normal density desktop screen.

3. Determine responsive image versions

Next, resize the window though all breakpoints and make a list of the widths the image goes through. Then add pixel density variants to their right.

Example:

Breakpoint x1 x1.5 x2 ← Pixel density
lg 1310 1965 2620 Standard image and retina image (see below)
md 1110 1665 2220 The versions above are still fine for these widths
sm+xs 890 1335 1780 1 version for mobile screens, those with Retina can still use 1310

The selected versions are: 890, 1310 and 1965. For an example image, their sizes are 110kB, 212kB and 430kB.

Because width matters way more than height in responsive design, the image sizes are denoted with the new w unit. Thus, the selected image sizes above are called 890w, 1310w and 1965w.

How to choose image versions

How many versions you choose depends on the degree of client optimization you want to achieve. More images means possibly better performance, but also more complexity on the server.

  • When image sizes are close (as in 10%), drop the smaller one. E.g. 1200, 1280 → 1280
  • There are reasonable limits for image size and density factor. Read on.

Reasonable lower limit

Almost all desktop browsers are used at widths beyond 1000px. Most mobile devices sport double-density screens that are 320px or more in width. Thus, images below 640w are not useful for full width images. (Images smaller than full-width on a 320px screen are mostly so small in file size that you generally should not need to create variants. Usually these images are quite small on desktop, too, so you might simply reuse the desktop image on mobile.)

A 640w JPG with 65kB would be only 20kB at 320w. While this looks like two thirds saved (wow!), it's only 45kB. So you'd create an extra image version just to save 45kB per image on one of the few non-retina tiny screens. Probably there are places where you can optimize better.

Reasonable factor limit

The intuitive upper resolution limit is the max size an image can get, multiplied by the screen's pixel density. However, since browsers are quite good at interpolating images nowadays, you do not need to feed every client pixel. Under most circumstances, 1.5 times larger will do fine for retina double-density devices. (I've manually tested this.)

For a 800w JPG and a 2x version of 300kB, the 1.5x version has 187kB. That's a 40% or 113kB reduction, which is quite much. Still, both variants are visually almost indistinguishable with only very sharp edges slightly blurrier in the smaller image. I call it "loss-less enough" ;-).

Considerations

The above limits and suggestions target the "average" case, where you want improve bandwidth usage and retina user experience with little effort. Of course you need to derive your own strategy depending on your audience.

4. Generate image versions

Now that you know which image widths you need, you'll have to generate them.

I'm using Carrierwave to store image uploads and to generate these versions. In the respective uploader, add image versions like this:

version :banner_1965 do
  process resize_to_fill: [1965, 600]
end
version :banner_1310, from_version: :banner_1965 do
  process resize_to_fill: [1310, 400]
end
version :banner_890, from_version: :banner_1310 do
  process resize_to_fill: [890, 272]
end

Affixing the versions with their width makes using them much easier, see below.

5. Render the image

We're almost done. However, the technically hardest part remains: How do we give the browser the right image variant? We cannot reliably know upfront which screen size he has, nor how large the image will be rendered.

Fortunately, a solution is already in the making. It has quite complex powers, but in the form we'll be using it consists of two additional attributes on the img tag: srcset and sizes.

srcset
Contains a list of image URLs together with their width: srcset="large.jpg 1965w, medium.jpg 1310w, small.jpg 890.jpg"
sizes
Contains a list of "reduced media queries" (which work slightly different from CSS) and the corresponding image size: sizes="(max-width: 900px) 500px, 1310px". An entry without a media query is taken as default value.

However, since I'm already using the awesome lazysizes (I can recommend this great library, btw) for lazy image loading, I'll also use it for managing responsive images. Because it lazy loads the image anyway, it can also select the appropriate size on the fly. Thus it makes the sizes attribute dispensable.

We'll use the following markup:

<img src="fallback/image.jpg" # Fallback image for old browsers
  class="lazyload" # Activate lazy image loading
  # A transparent 1px gif that keeps modern browsers from loading the src
  srcset="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
  data-sizes="auto" # Tell lazysizes to manage the `sizes` attribute
  data-srcset="large.jpg 1965w, medium.jpg 1310w, small.jpg 890px" />

To generate this in Rails, we'll use the following helper …

# Expects the version names to end with _<image width>, e.g. banner_1965
def responsive_image_tag(uploader, versions:)
  url_widths = versions.map do |version|
    url = uploader.send(version).url
    width = version[/_(\d+)$/, 1]
  
    [url, width]
  end
  srcset = url_widths.map { |url, width| "#{ url } #{ width }w" }.join(', ')
  
  image_tag url_widths.first[0], # lazysizes recommends src == data-srcset[0]
    class: 'lazyload',
    # This transparent gif keeps modern browsers from early loading the
    # fallback `src` that is only for old browsers
    srcset: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
    data: {
      srcset: srcset,
      sizes: 'auto',
    }
end

… and use it like this:

= responsive_image_tag @page.image, versions: %i[banner_1965 banner_1310 banner_890]

Voilà! (This example image does not use lazysizes because it is not installed on this blog. Nontheless it's using the srcset and sizes attributes to have the browser choose the appropriate version.)

Conclusion

We've built a simple system of responsive images that delivers appropriate image sizes to each client. It is as easy as determining the required image widths, defining them in a Carrierwave uploader and rendering them with the supplied helper method.

I hope you enjoyed this post. Send me a note when you've incorporated responsive images somewhere!

Resources

Growing Rails Applications in Practice
Check out our e-book:
Learn to structure large Ruby on Rails codebases with the tools you already know and love.

Recent posts

Our address:
makandra GmbH
Werner-von-Siemens-Str. 6
86159 Augsburg
Germany
Contact us:
+49 821 58866 180
info@makandra.de
Commercial register court:
Augsburg Municipal Court
Register number:
HRB 24202
Sales tax identification number:
DE243555898
Chief executive officers:
Henning Koch
Thomas Eisenbarth