Perfection Kills

by kangax

Exploring Javascript by example

← back 772 words

Proto.Lazy – do we really need lazy image loading?

It looks like the “Lazy load” plugin for jQuery (released about a week ago) got quite of attention. I personally don’t find it much useful but the idea is pretty cool. Can we do something like this with prototype? Easy! Let’s see how.

One of the things I found to be challenging was calculating whether element is positioned within viewport. Prototype does not have such method (as far as I know) but it does provide us with something that makes it quite trivial. Let’s look at ingredients:

Note: The following snippets require prototype 1.6rc0 or higher


// returns viewport dimensions
// alternatively there are explicit getHeight() and getWidth() methods
document.viewport.getDimensions(); // {width: 1024, height: 768}

// returns scroll offsets of viewport (how much the page was scrolled)
document.viewport.getScrollOffsets(); // [0, 10]

// returns element's position relative to a PAGE
element.cumulativeOffset(); // [120, 560]

// getDimensions() - alternative for getHeight/getWidth (in the end I'll explain why use one over another):
element.getDimensions(); // {width: 100, height: 310}

OK. Those only look scary, but are actually quite simple to use.

I decided to wrap the entire script into “Proto” namespace to be nice with window object (you are wrapping your stuff into namespace, aren’t you?) and define “Lazy” class in that namespace. Two actions will trigger image loading – window’s scroll and resize events (I’m not sure if original plugin checks window for resize but I think it makes more sense this way). Alternatively, if “event: ‘click’” option is set, pictures will be revealed on click (ignoring window’s events). Here’s a short breakdown of what’s going on:

  • Iterate over all images on a page.
  • If image is NOT within viewport at the moment, empty its “src” attribute and store it as a custom property of an element (this prevents it from being loaded).
  • If event: ‘click’ is set, attach click handler to reveal image else:
  • Attach event handlers to window’s scroll and resize events
  • Once any of window events occur (scrolled or resized) check whether image is within viewport and if so, put its original “src” attribute back from custom property, delete that property.

That’s all there is to it, 40 lines of happyness…


if (Object.isUndefined(Proto)) {var Proto = {}}
  Proto.Lazy = Class.create({
    initialize: function(options) {
      this.options = options || {};
      $$('img').each(function(el){
        if (!this.withinViewport(el)) {
          el._src = el.src;
          el.src = this.options.placeHolder || '';
          if (this.options.event === 'click') {
            el.observe('click', function(){
              if (this._src) { this.src = this._src; delete this._src }
            });
          }
        }
      }.bind(this));

      if (this.options.event !== 'click') {
        Event.observe(window, 'scroll', this.load.bind(this));
        Event.observe(window, 'resize', this.load.bind(this));
      }
    },
    load: function(el) {
      $$('img').each(function(el){
        if (el._src && this.withinViewport(el)) {
          el.src = el._src;
          delete el._src;
        }
      }.bind(this));
    },
    withinViewport: function(el) {
      var elOffset = el.cumulativeOffset(),
          vpOffset = document.viewport.getScrollOffsets(),
          elDim = el.getDimensions(),
          vpDim = document.viewport.getDimensions();
      if (elOffset[1] + elDim.height < vpOffset[1] ||
          elOffset[1] > vpOffset[1] + vpDim.height ||
          elOffset[0] + elDim.width < vpOffset[0]  ||
          elOffset[0] > vpOffset[0] + vpDim.width) {
        return false;
      }
      return true;
    }
});

After including Proto.Lazy.js, all that’s left to do is instantiate object (preferably before images are loaded which is right after ‘contentloaded’ event is fired):


document.observe('contentloaded', function(){
  new Proto.Lazy();
  // new Proto.Lazy({ event: 'click' });
  // new Proto.Lazy({ placeHolder: 'images/default.png' });
});

Here’s a simple demo page.

It’s pretty clear that withinViewport function should be fast, since it’s being called on EVERY scroll/resize event for EVERY image. That’s a LOT of calls, considering the dynamic nature of resize/scroll events. Knowing that getHeight() and getWidth() methods actually invoke getDimensions() method (and read corresponding property from returned object), we could save some time by calling getDimesnions() explicitly and store returned object for later use.

I also have a feeling that image iteration could be done in a more efficient way. Maybe it would make sense to break out of a loop once first hidden image is found, since viewport is always occupying few closely positioned images. The problem though is that this would only work with images positioned linearly and in the same order as defined in a document.
In the end, I’m not quite sure how fast this implementation is (some tests with bunch of thumbnails would be nice to have) but it seems to be doing its job.

I hope this was a good example of defining a class in prototype as well as working with viewport and element positioning. I also encourage everyone to try to think about speed issues when developing any kind of user interfaces (no, seriously, nothing is more annoying than slow and unresponsive app).

Happy prototyping!

Did you like this? Donations are welcome

comments powered by Disqus