A. M. Douglas

Lazyloading CSS background-images

As a rule, I generally prefer, for the purposes of accessibility and SEO, to use IMG elements to display images over the background-image approach. However, getting an image to display in a certain way — say, in such a way that it expands to fill a given space while maintaining aspect ratio — is not a trivial matter, and the solution (much the same as my prior demonstration of making a video background) raises some concerns for users of older browsers.

While not conceptually difficult, it is also more time-consuming and potentially buggy to implement as opposed to the obvious answer: background-size: cover (which is equivalent to background-size: auto 100%, if you were wondering). CSS background images offer a very convenient and malleable way to display images, and if you don’t care about your images being picked up by screen readers and Google Image Search, why not use them?

Well, it might also appear to be a bit of a bummer that most prêt-à-porter image lazy-loading snippets/‘plugins’ are intended for loading IMG elements. I’m not sure why this is the case, since it seems straightforward to lazy-load a background-image by applying it as an attribute style, having retrieved the image source from, say, a data- attribute.

Since I couldn’t find a JS lib that would do both, I rolled my own, which I called toad because it leaps into action (and because toad rhymes with load, obviously). The key to creating a library to do both is to test whether an image source should be cloned to the src attribute or as a background-image in an attribute style.

The easiest way to discern whether you need to apply a background-image style is to test the element in question and see if it’s an IMG element. Your function to test for this might look like this:


function isImg( element ) {
  if ( !element || el.nodeType !== 1 )   return false;
  if ( el.tagName !== 'IMG' || !el.src ) return false;
  return true;
}

First, make sure you have a something. Make sure it’s a valid element while you’re at it. Then make sure it’s an IMG element. And finally, may as well test to see if the image has already been loaded and send that down as the boolean that will decide ultimately whether you lazy-load or not.

It’s also important, however, to make sure you don't overwrite the background-images you've already set. To do this, we want to loop through the element’s style attribute and check if background-image is anywhere in there. This is all we need to check, because if background is set using the short-hand, it will overwrite the background-image anyway.

Once you’ve discerned how to load the image, you need to make sure that if an element already has a source, be it src or url(), you need to remove that element’s data- attribute. This will prevent your lazy-loader from firing when it does not need to.

Finally, something you should definitely have is a means of throttling the scroll and resize events. lodash and underscore both offer well-tested and robust throttle and debounce functions which can simply wrap other functions. However, I wanted to use requestAnimationFrame, and I came up with a handy little ditty to enable a similarly easy means to wrap functions for a kind of auto-throttle.


function rebounce(f){
  var scheduled, context, args, len, i;

  return function(){
    context = this;
    args = [];

    for(var i = 0; i < arguments.length; ++i){
      args[i] = arguments[i];
    }

    window.cancelAnimationFrame(scheduled);

    scheduled = window.requestAnimationFrame(function(){
      f.apply(context, args);
      scheduled = null;
    });
  }
}

Note how we do not touch arguments directly, only via arguments.length or arguments[n]—this ensures that V8 still optimises our code at runtime.

This then enables you to apply your scroll handler in this very simple way:


window.addEventListener( 'load',   lazyload, false );
window.addEventListener( 'scroll', rebounce( lazyload ), false );
window.addEventListener( 'resize', rebounce( lazyload ), false );

Nota bene: lazy-load initially on DOMContentLoaded or load/onload, because you’ll presumably have removed all of your src attributes from your markup, leaving blank space without triggering the first lazy-load as soon as the document is ready.

Final tip: obviously removing all of your src attributes and whatnot will cause an issue for users who do not have JavaScript enabled. To solve this problem, include all of your images in the normal way but wrapped in noscript elements, like ghostly twins to your lazy-loaded elements.

You can see my lazy-loading implementation in action here · Full source on Github.

Labels: ,

0 Comments:

Post a Comment