QR Code contains TinyURL of this article.Silky-Smooth Image Loading

artist, with a selection of his work

In a recent article, I described how I’d finally addressed the biggest irritant I had with responsive images: page reflow on image load. I am happy with how that turned out. Then I was witness to Mike Davidson’s redesign of his website, Mike Industries. I was immediately jealous of the Medium-esque method with which he was loading his hero images. I just had to steal take inspiration from his technique and deploy it on the Perpetual βeta.

With this technique, image loading passes through three stages:

  1. loading;
  2. loaded;
  3. transition.

The aim is to render each of these stages in a manner that’s aesthetically pleasing, without compromising on the page-footprint, performance or user experience.

The methodology is to render a light-weight, low-resolution placeholder while the full-size image loads, then — once loaded — transition to the principal image smoothly.

Davidson achieves this by loading a small placeholder image that the browser scales-up to fill the container. It looks great and really caught my attention when I first saw it. But, for me, there are two deal-breaking problems with Davidson’s implementation:

  1. an additional network request for the small image;1
  2. fails when JavaScript is not available.

We can easily dispense with the additional network request if we inline a base-64 encoded representation of our background image. We can get away with this because the images are so small. For example, the hero image at the top of this page is just 995-bytes when base-64 encoded:2

<div style="background-image: url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2OTApLCBxdWFsaXR5ID0gNzUK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgABwAKAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8AjPhKae7uY4bOBLNX3O3ntnHI2AfXv7VTbwTcbj5YTZn5d0xzjtniiisTU//Z'); padding-bottom: calc((547/820)*100%);"></div>

For the Lazy Developer

In an earlier article I described how I automated the production of the image mark-up for the Perpetual βeta. Adding base-64 encoding of the background image required only minor modifications to my srcset_factory code.

Here’s the modified Gist.

Furthermore, as this is a string, it benefits from compression on the server prior to delivery and, due to it being inline, the browser can render it without delay. Win, win.


Which brings us to the second item. Why does Davidson’s hero image not load in the absence of JavaScript?

Actually it does load, but it’s initial CSS state is opacity: 0; (i.e. transparent) so that it is invisible while it is loading. Davidson then uses JavaScript to trigger a CSS transition to opacity: 1; when loading is complete. Unfortunately, he does this without any fallback for when JavaScript is unavailable.

So all I needed was to ensure that my images would display irrespective of JavaScript, then use JS to prettify the process. Progressive enhancement all the way.

The markup for this page’s hero image looks like this (abridged for brevity):

<figure style="max-width: 100vw;">
  <div style="background-image: url('data:image/jpeg;base64…'); padding-bottom: calc((547/820)*100%);" class="image-loader">
    <img alt="artist, with a selection of his work" src="/assets/images/480/d9b5029a0544d34b09b1807e9eb7ad79.jpg" srcset="/assets/images/80/d9b5029a0544d34b09b1807e9eb7ad79.jpg 80w, … /assets/images/1640/d9b5029a0544d34b09b1807e9eb7ad79.jpg 1640w" sizes="100vw" />
  </div>
</figure>

It should be clear from the above that, in this form, a lack of JavaScript should not prevent either the background or the full-size image loading. Without JS, the load process of the principal image will be visible (and not pretty), but the browser will at least render the image.

For the enhancements, there are two things to note about the above markup:

  1. the image-loader class on the container;
  2. the onload attribute on the img tag.

The SCSS definitions for .image-loader are:

.image-loader {
  background: inherit;
  background-size: cover;
  background-repeat: no-repeat;
  width: 100%;
  display: inline-block;
  overflow: hidden !important;
  height: 0;
  position: relative;
  img {
    position: absolute;
  }
}

At the end of my HTML I have the following JavaScript:

<script type="text/javascript">
//<![CDATA[
  // get all 'image-loader' containers
  function imageLoader() {
    var containers = document.getElementsByClassName('image-loader');
    var containerList = Array.prototype.slice.call(containers);
    containerList.forEach(getImage);
  }

  // get all images from within 'image-loader' containers
  function getImage(element) {
    var images = element.getElementsByTagName('img');
    var imageList = Array.prototype.slice.call(images);
    imageList.forEach(addImageLoadingClass);
  }

  // add 'image-loading' class
  function addImageLoadingClass(element) {
    element.classList.add('image-loading');
  }

  // launch the image loader
  (function() {
    imageLoader();
  })();
//]]>
</script>

When the HTML finishes loading, the browser executes the imageLoader() method. This code finds all tags that have the class image-loader, then all img tags within and adds the class image-loading to each of them.

The CSS for .image-loading looks like this:

.image-loading {
  opacity: 0;
}

What’s important to note here is that, unlike Davidson’s implementation, my images do not acquire the opacity: 0; definition unless JavaScript is available, since we use JS to apply the corresponding class. But we have to do this quickly — it must be imperceptible to the user — which is why we inline the JS as opposed to loading it in with a separate network request. Clever huh?

At this point, we’re in a position where our background and principal images both load, with or without the availability of JavaScript, and if JS is available our principal image will be transparent while it loads.

Now we need to transition from the image-loading to the image-loaded state and that’s where the onload="imageLoaded(this)" attribute, that I drew your attention to earlier, comes into play.

“For all of you youngsters that have never seen one of these, it’s called an inline script attribute, and allows us to bind JavaScript functionality directly to events triggered from DOM elements, much in the same way that we can add styling directly to elements using the inline ‘style’ attribute. Believe it or not, these inline script attributes were a huge part of writing JavaScript in the early days of the web, and like inline styles, are generally frowned-upon today by semantics-nazis.”

“So for the rest of you who are about to run away in disgust at the sight of inline JavaScript, please stick around and take my word that this is still the single most efficient and bulletproof method of capturing the ‘load’ event of an image in the DOM. While I’m all for progress and HTML‍5 — I have absolutely nothing against using old-school techniques if they are still elegant and functional.”

“The alternative to this, would be to individually bind the load event to each image on ‘document ready’. The problem arises however, when images load before ‘document ready’ fires, and before we have time to bind our functionality to each image’s load event. This is a particular issue when images are already cached by the browser from a previous session, and load instantly. We miss the event, and our function is never called. The ‘onload’ attribute has none of these issues as it is ‘pre-bound’ to the event, so to speak, and is therefore processed as the browser parses the HTML.” Patrick Kunka: Taking Control of Image Loading

I had my doubts about Kunka’s assertion that the onload attribute is the “single most efficient and bulletproof method of capturing the ‘load’ event of an image in the DOM.” I wasted an entire, frustrating day trying to prove otherwise. Suffice it to say, I am now a believer.

The last thing inside the <head> tag of my pages then is the imageLoaded() method that the onload events trigger:3

<script type="text/javascript">
//<![CDATA[
  // swap 'image-loading' class for 'image-loaded'
  function imageLoaded(img){
    img.classList.add('image-loaded');
    img.classList.remove('image-loading');
  };
//]]>
</script>

This script’s purpose is simple, add an image-loaded class to the image object it receives and remove the existing image-loading class.

We define the image-loaded class as follows:

.image-loaded {
  opacity: 1;
  transition: opacity 500ms;
  transform: translate3d(0, 0, 0);
}

which transitions the principal image smoothly to opacity: 1; over the course of a half-second.

The result is barely noticeable on a fast connection. On a slow one, it makes all the difference in the world and offers a better user experience in my opinion.

It’s worth noting that this technique is not appropriate for every image. If an image has background transparency or it’s background is the same colour as the page it sits on, then the result can sometimes look a little weird.

In these cases I disregard the scaled-up background image and use an SVG spinner instead.4  You can see an example of this with the hero image on my article, “Responsive Images Without Browser Reflow.” In other instances, a simple background-color is sufficient.

I’m sure there are other options too.

  1. The additional network request is not a problem with an HTTP/2-based site — like the Perpetual βeta — but, at the time of writing, Mike Industries delivers over HTTP 1.1 ↩︎

  2. Other background images on this website are less than half that size. ↩︎

  3. It’s crucial that this script is inside the <head> tags as it must be available when the first onload event fires. ↩︎

  4. My SVG spinner Gist↩︎