Perfection Kills

by kangax

Exploring Javascript by example

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

September 11th, 2007

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!

Categories: Class.create, document.viewport, Proto.Lazy

Comments (15)

  1. Gravatar

    AntonioCS said on Sep 12, 2007 @ 2:25

    Great snippet!

    I have tried to find reference to:

    document.viewport.getDimensions();
    document.viewport.getScrollOffsets();
    element.cumulativeOffset();
    element.getDimensions();

    I tried google and then went directly to the prototype api page. Can’t find anything. Can you give some links please?

    Also the event contentloaded does not show up on the DOM page for events (the DOM of the MDC)

  2. Gravatar

    Nicolas Sanguinetti said on Sep 12, 2007 @ 7:56

    Sweet! Will probably try it out — as soon as I port my job’s code to 1.6! :)

    @Antonio: the document.viewport and Element methods you seek are provided by prototype 1.6 (still a release candidate, hence no googlable documentation) as cross browser solutions.

    The ‘contentloaded’ event is a wrapper for DOMContentLoaded, DOMReady, et al. It’s not on MDC’s wiki because there’s no such event on Firefox. Furthermore, this event isn’t even a W3C standard, but Prototype makes it work across browsers “naturally”.

    (As other libraries do, but we are talking about Prototype here ;))

  3. Gravatar

    AntonioCS said on Sep 12, 2007 @ 14:09

    hhumm ok ok so the only way I could have found about these was checking out the code??

  4. Gravatar

    kangax (article author) said on Sep 12, 2007 @ 14:18

    @Antonio
    hhumm ok ok so the only way I could have found about these was checking out the code??
    As of now, yes.
    1.6 is not yet final and many new features are not officially documented. I happen to use source as a reference (instead of API docs) as it usually makes things more clear.
    Even though I believe that current RC is quite stable, I suggest you don’t use it in critical environment :)

  5. Gravatar

    Samuel Lebeau said on Sep 12, 2007 @ 16:49

    This is nice ! However it doesn’t seem to work on Safari 2, maybe you should use read/writeAttribute for ‘src’, not sure…

    @Antonio: You can find some documentation on the Post on Prototype blog

  6. Gravatar

    Mika Tuupola said on Sep 15, 2007 @ 3:56

    Great work on the Prototype port!

    Note that you should remove the src attribute. If you only empty it IE will make a request to index of the folder.

    Lets assume lazy loaded page is /foo/bar.html. You only empty the src attribute. IE will now make request to /foo/ for each image with empty src.

    When you completely remove src attribute this will not happen.

  7. Gravatar

    Olaparent said on Sep 25, 2007 @ 3:48

    Interesting but I would have loved seeing the demo page.
    But it seems to be just blank and empty ?!

    Thanks

  8. Gravatar

    Iwo said on Oct 25, 2007 @ 6:25

    You can check out a demo of a very similar code at ToolSoup’s Web Gallery.

  9. Gravatar

    louis w said on Jan 23, 2008 @ 12:59

    What happened to the demo page? 404

  10. Gravatar

    Matt said on Sep 5, 2008 @ 23:16

    Yeah, I’d really like to see youe demo page, but it’s 404.

  11. Gravatar

    cwxwwwxdfvwwxwx said on Dec 24, 2008 @ 18:11

    well, hi admin adn people nice forum indeed. how’s life? hope it’s introduce branch ;)

  12. Gravatar

    Nikita Vasilyev said on Oct 3, 2010 @ 8:10

    Link to the demo page is broken.

  13. Gravatar

    Patrick James Fontillas said on Jun 17, 2011 @ 11:20

    Instead of attaching the load fn called on every resize and scroll you can stop observing the load fn on resize and scroll and use a timeout to start observing it again a certain time later. You’ll have to play with the timing to find one you like.

    One issue came up where if you set the timeout too high by the resize and scroll events are being observed again you’ve stopped scrolling or resizing the window. In that case the image wouldn’t load until you started scrolling again (if you even do that). My way around that issue is that at the end of the timeout you not only start observing the resize and scroll events again but you also do one last check regardless of whether we are still scrolling or resizing.

    You’ll find that this method can reduce the number of calls from 50+ a second to 4-10 depending on your timeout length.

  14. Gravatar

    Patrick James Fontillas said on Jun 17, 2011 @ 11:21

    Wish I could edit comments…

    *One issue I ran into is where if you set the timeout too high by the time the resize and scroll events…

Trackbacks

  1. Rails, Rails Plugins, JavaScript, SEO « exceptionz said:

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

Leave a Comment

Please, don't forget to escape your input (<, > and &). Wrap code sections with <pre>

Allowed tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>