Daniel Groves

Show Nav

Serving Responsive Images with on-the-fly Rendering

Published: 1 May 2016 · Tags: images, development, responsive

Using Imgix to dynamically render responsive images on the fly.

A problem I’ve been looking for a solution to for a little while now is how to approach serving images on this website. Many of the posts, particularly in the adventures and photography section, have lots of high-resolution images. These images are around 1500px wide but many devices have screens significantly smaller than this, so a lot of loading time and bandwidth is wasted. The solution is to use responsive images.

Choosing the right method

Before we can choose the right method to serve the images, we need to understand exactly what the requirements are. In my case I had a criteria I wanted to get as close to as possible:

  • I wanted a single source for all instances of an image (not a directory per post as before). This increases the likelihood of there being a hit on the CDN and speeds up serving the image.
  • I wanted to step image sizes to the nearest 100px, once again the increase the likelihood of a CDN hit, whilst minimising wasted data.
  • I wanted to remove all master images from the repository my website lives in. It was starting to get a bit too big for my liking, and this would increase my options in the future.
  • Ideally I wanted to serve WebP images to browsers with support to take advantages of the vastly more efficient compression.

There’s currently three different primary methods to serve responsive images, each with their own advantages and disadvantages. Let’s take a look:

srcset

Use of the srcset attribute allows you, the developer, to provide additional images for a browser to choose from. The idea here is that you provide a range of images at different resolutions and the browser will pick the best one to display. Being an extra attribute on the normal img tag probably means this would be the easiest to integrate into a legacy site.

The HTML in the page would look something like this:

<img src="image-one-small.jpg" srcset="image-one-medium.jpg 700w, image-one-large.jpg 1000w" alt="Image One">

You can add as many different srcset options as you like, however each of these images would have to be pre-generated. This is something I wanted to avoid, but we’ll look at different approaches to generating images later.

picture/source

Using picture and source together is slightly more complex than srcset, however it is far more flexible. As shown in the example below is allows you to add media queries into the mix, and still allows you to provide a standard fallback image which will always display in a worst case scenario.

<picture>
  <source media="(min-width: 400px)" srcset="image-one-medium.jpg 1x, image-one-big.jpg 2x">
  <source srcset="image-one-small.jpg 1x, image-one-medium.jpg 2x">
  <img src="image-one.jpg" alt="">
</picture>

In my situation, the pitfalls here are essentially the same as with srcset: I have to generate a lot of images up-front, and it’s difficult to serve different images format to different browsers.

data-src

Using the data-src attribute (you can call this whatever you like, I just like data-src as it’s obvious what it represents) is by far the most flexible approach. You essentially have a normal image but replace the src with a data-src attribute instead. You then use JavaScript to inject the real src attribute when you’ve programatically decided it’s the right time to do so, and what you’d like to load.

We can still support fallback images with ease as well, thanks to the html noscript tag.

<img data-src="image1.jpg" alt="Image 1">
<noscript>
  <img src="image1.jpg" alt="Image 1" />
</noscript>

I like this approach more than the others thanks to the additional flexibility. As the src is set dynamically at runtime I can inject additional information which could be interpreted by a remote service such as a width and height. The final img tag might end up looking something like this:

<img src="image1.jpg?width=1500&height=800&format=webp" data-src="image1.jpg" alt="Image 1">

This opens up the world of producing renders on-the-fly using a server-side service based on the given URL parameters.

Optimus

I’ve already mentioned that I didn’t want to generate all of the possible image combinations at build time. I did experiment with this pretty early-on, however I found that even with what was, at the time, a pretty minimal image set I was waiting a good 10-minutes for the images to be generated. This was always going to grow directly with the number of images I add. I’m regularly adding new posts with a significant number of images, so realistically this was never going to scale into the future.

Having ruled out pre-generated images entirely I decided to look into dynamically rendering images on-the-fly. The idea here would be to have a “bucket” of master images which would be used as the source, and for image requests to have a width and height appended. I’d then be able to resize the image to these dimensions, cache the rendered image on my Cloudfront CDN and serve this to future clients with the same screen properties.

I wrote a Rack application called Optimus to generate these images on-the-fly. As I suspected at the start the performance simply wasn’t there, and the best way to speed this up was going to be doing the renders on a GPU, however cloud GPU instances are expensive. Unfortunately, this site does not pay the bills (it doesn’t actually pay me anything right now) so spending $100+ on a GPU instance is not realistic.

Introducing Imgix

After some digging around a friend eventually recommended looking into Imgix. Imgix essentially does what I wanted to achieve with Optimus, only on dedicated hardware and as a service. It’s cheap too, with plans starting from $10 a month. After having a look around to see what their query-string based API was capable of I decided to integrate it into my site and see how it performed.

At this point I re-exported every single image from Lightroom into a single directory with a naming convention (year-month-web-original_name.jpg) which I then synchronised with an Amazon S3 bucket using Transmit. I created a read-only user for Imgix, which can use an S3 bucket as an origin source.

Imgix also provides a lightweight JavaScript library to handle client-side images. This couldn’t be simpler to work with as it simply requires a data-src attribute and a class for it to hook into. The markup on my site around images is generated through a plugin I wrote for Jekyll, so it was simply a case of adding this class in the output HTML and adding the configuration for Imgix to the page.

imgix.onready(function() {
  imgix.fluid({
    lazyLoad: true,
    lazyLoadOffsetVertical: 500,
    pixelStep: 100,
    debounce: 200,
    updateOnResize: true,
    updateOnResizeDown: false,
    updateOnPinchZoom: false,
  });
});

The Imgix library provides built in lazy loading and image reloading if required. I have configured both of these, and my configuration is easily explained:

  • lazyLoad: true: Lazy load images os they not downloaded until the user is close to them on the page.
  • lazyLoadOffsetVertical: 500: Load images when the user is 500px away from them in vertical scrolling.
  • pixelStep: 100: Round the size of images up to the nearest 100px.
  • debounce: 200: Wait for the user to pause for 200ms before requesting resized images.
  • updateOnResize: true: Request new images if the user reloads their browser window.
  • updateOnResizeDown: false: Don’t request new images if the user makes their window smaller.
  • updateOnPinchZoom: false: Don’t request new images if they use pinch zoom (such as on an iPhone or iPad).

Lazy loading the images like this brings in the advantage of saving further bandwidth for any users that don’t scroll all the way to the bottom of a piece of content. This also provides a much faster initial load time as the browser will not wait for images to complete downloading.

One limitation of the Imgix library is that it will not submit the height of an image when it requests it, only the width. This resulted in post list thumbnails looking awkward as the 4:3 image ratio doesn’t suit the layout of these pages. This is easily handled as the library allows us to modify the request parameters before they go out. This is simply done by creating a callback function:

imgix.onready(function() {
  imgix.fluid({
    lazyLoad: true,
    lazyLoadOffsetVertical: 500,
    pixelStep: 100,
    debounce: 200,
    updateOnResize: true,
    updateOnResizeDown: false,
    updateOnPinchZoom: false,
    onChangeParamOverride: function(width, height, options, element) {
      if (element.attributes.height !== undefined)
        options.h = parseInt(element.attributes.height.value)

      return options;
    }
  });
});

This callback simply grabs the node for the image currently being handled and pulls the height from it if it has been set. It then sets the query parameter h to it’s value, which is what the Imgix API requires if you wish to set the height on the image.

While we’re modifying the image request parameters we can set the auto: format parameter which tells Imgix to detect the best image format for the current browser and serve the images in that format1. In reality these means you will get WebP images in Chrome and Opera, but otherwise you will get progressive JPEG images.

imgix.onready(function() {
  imgix.fluid({
    lazyLoad: true,
    lazyLoadOffsetVertical: 500,
    pixelStep: 100,
    debounce: 200,
    updateOnResize: true,
    updateOnResizeDown: false,
    updateOnPinchZoom: false,
    onChangeParamOverride: function(width, height, options, element) {
      if (element.attributes.height !== undefined)
        options.h = parseInt(element.attributes.height.value)

      options.auto = 'format';
      return options;
    }
  });
});

This is now enough for the main images for any page to be handled by Imgix, however I also use header images for many pages which are actually background images. This allows me to set the size of the container as a percentage of the users screen, and to let the browser control the image size and position to best fill that area.

I had to make some minor modifications to this to allow Imgix to handle the images, which now looks like this:

<div class="image imgix-fluid-bg" data-src="https://danielgroves-net-2.imgix.net/2016-03-web-DSCF6107.jpg"></div>

This simply sits at the top any page with a header image just inside the <main> tag. This also requires a second snippet to initiate the Imgix library for the headers, as I wanted to have different settings for these images:

imgix.onready(function() {
  imgix.fluid({
    fluidClass: 'imgix-fluid-bg',
    updateOnResizeDown: true,
    updateOnPinchZoom: true,
    pixelStep: 100,
    autoInsertCSSBestPractices: true
  });
});

The addition of the autoInsertCSSBestPractices: true option allows the Imgix library to automatically inject any CSS it requires to setup the image as a background.

All of these didn’t take as long as you might think to do, I spent maybe an hour total setting up Imgix and a few hours preparing my content for the new origin location. The question is, how much of a difference has this actually made?

Testing the results

Testing the performance gains from making these changes is easy enough. Before deploying the new version of the site I disabled the cache in Google Chrome and went through the site taking screenshots of the developer tools for several of the pages. I repeated this exercise afterwards to demonstrate the improvement.

I tested three different pages each with different sizes and volumes of images associated. The first test is using Soar as an example. The results from the developer tools show a clear improvement:

  • DOM Content Load: 474ms down to 359ms, an improvement of 115ms.
  • Finished Loading: 6.79s down to 1.3s, an improvement of 5.49s.
  • Data transfer: 4.2MB down to 785kb, an improvement of 3.44MB.
 alt: Before responsive images result for Soar  alt: After responsive images result for Soar
Before then after introducing responsive images for Soar.

Next I tested the Adventures and Photography post listing. This page has a series of small thumbnails. Before introducing Imgix I was manually generating these at roughly the right size, now they’re done dynamically. Once again we’ve seen a significant improvement.

  • DOM Content Load: 627ms down to 431ms, an improvement of 196ms.
  • Finished Loading: 2.54s down to 1.35s, an improvement of 1.19s.
  • Data transfer: 1.5MB down to 776kb, an improvement of 0.75MB.
 alt: Before responsive images result for Adventures and Photography  alt: After responsive images result for Adventures and Photography
Before then after introducing responsive images for Adventures and Photography.

The final page was Wind, Rain and Mountains. In this case we’ve also got an embedded map to contest with that loads a lot of images that are outside of my control. In this case we saw a total of 5.1MB reduced to 1.5MB. This could be reduced further in the future by lazy loading the mapping iframe as and when it is required.

  • DOM Content Load: 210ms down to 369ms, a decrease of 159ms.
  • Finished Loading: 6.79s down to 2.99s, an improvement of 3.8s.
  • Data transfer: 5.1MB down to 1.5MB, an improvement of 3.6MB.
 alt: Before responsive images result for Wind  alt: After responsive images result for Wind
Before then after introducing responsive images for Wind, Rain and Mountains.

Conclusions

For something which only took a few hours to put together, including setting up a new origin location, the improvements seen here are massive. This is seems to be working well so far, but it will be interesting to see how it works in the long term.

I firmly believe this was the right solution for me. With more and more content going up on my website regularly, almost all of which is image heavy, pre-generating images would only ever have become more and more of a hindrance. For me performance is becoming more and more of an issue as I slowly work towards a bigger endgame.

If you’re serving responsive images on your website please do let me know what solution you’ve picked. I’d be very interested to hear how you’re getting along and if you’d change it in the future.

  1. Unfortunatly this cannot be set in the control panel as a “default parameter” like all of the other properties. After talking to the Imgix support team, they have confirmed they’re working on removing this limitation from their current stack.