Quantcast
Channel: frag (frăg)
Viewing all articles
Browse latest Browse all 40

Preloading images using javascript, the right way and without frameworks

$
0
0

There are two scenarios here. Case 1: you just want to load all your images prior to displaying the rest of your page. Whether that’s because you like it this way or you need to get the browser to ‘know’ your images widths and heights first, preload a gallery of images, it does not matter. The arguments for ‘speeding up’ page loads are a bit moot now, with modern connections and modern browsers there are no great benefits to this. Either way, you need to arrange your code in a way that allows you to trigger some event or function when the loading is complete in order to continue displaying your site.

And then, there’s case 2: pre-emptive image loading / cache priming, which is the more interesting use. This is a relatively new trend that involves examining the user’s browsing pattern and anticipating what they do next. For instance, if they are browsing product listings as a result of a search with 100 results (20 per page), it is almost safe to assume they may want to go to page 2 or 3. It is also safe to assume the average user would take a while to finish appraising their results, scroll down and locate the ‘next’ link. This idle slot is where you can put their connection to better use by priming the browser cache with the product thumbnails of the next results page. In this instance, you don’t really care for any onload / oncomplete events, the benefits of the cache will be reaped on the next page instead.

Alternatives for priming cache are available for HTML5 browsers!
You can now use the HTML5 link prefetching API: MDN Link Prefetching

<link rel="prefetch" href="/images/big.jpeg">

Now, we’ve established why you may want to preload images, let’s focus on the how instead.

There are some considerations we need to be aware of here:

  1. It needs to be threaded in parallel (to use the browser’s maximum threads, normally 6 per domain)
  2. It needs to also be sequential (pipelined) for when you want to use a single thread
  3. It needs to handle individual image events – onload, onabort, onerror
  4. Work cross-browser and without any dependencies
  5. We need to clean up resources and memory, not leak memory and prevent IE GIF onload from firing multiple times

First of all, it’s important to understand that you need to assign the onload event BEFORE you set the src property of the image object:

// a common mistake seen, adding event after src assign:
var image = new Image();
image.src = 'image.jpg';
image.onload = function(){ 
    // won't work if image already cached, it won't trigger
    // ...
};

// correct way:
var image = new Image();
image.onload = function(){ // always fires the event.
    // ...
};
// handle failure
image.onerror = function(){

};
image.src = 'image.jpg';

And second, in Internet Explorer 5/6/7, the onload event fires ALL THE TIME for animated GIFs. That’s right, on each animation loop it fires an onload for you. Which is why you need to make sure you release your image objects for GC by not saving a reference – or should you need to store them as objects into a new array – remove all onload and other events attached to them.

Anyway, without further ado, the code.

See the this in action at this jsfiddle, now also available as a repository on GitHub: pre-loader. Please note the demo needs a HTML5 browser but the preLoader class does not and should work IE6 and up.

// define a small preLoader class.
(function(){
    'use strict';

    var preLoader = function(images, options){
        this.options = {
            pipeline: false,
            auto: true,
            /* onProgress: function(){}, */
            /* onError: function(){}, */
            onComplete: function(){}
        };

        options && typeof options == 'object' && this.setOptions(options);

        this.addQueue(images);
        this.queue.length && this.options.auto && this.processQueue();
    };

    preLoader.prototype.setOptions = function(options){
        // shallow copy
        var o = this.options,
            key;

        for (key in options) options.hasOwnProperty(key) && (o[key] = options[key]);

        return this;
    };

    preLoader.prototype.addQueue = function(images){
        // stores a local array, dereferenced from original
        this.queue = images.slice();

        return this;
    };

    preLoader.prototype.reset = function(){
        // reset the arrays
        this.completed = [];
        this.errors = [];

        return this;
    };

    preLoader.prototype.load = function(src, index){
        var image = new Image(),
            self = this,
            o = this.options;

        // set some event handlers
        image.onerror = image.onabort = function(){
            this.onerror = this.onabort = this.onload = null;

            self.errors.push(src);
            o.onError && o.onError.call(self, src);
            checkProgress.call(self, src);
            o.pipeline && self.loadNext(index);
        };

        image.onload = function(){
            this.onerror = this.onabort = this.onload = null;

            // store progress. this === image
            self.completed.push(src); // this.src may differ
            checkProgress.call(self, src, this);
            o.pipeline && self.loadNext(index);
        };

        // actually load
        image.src = src;

        return this;
    };

    preLoader.prototype.loadNext = function(index){
        // when pipeline loading is enabled, calls next item
        index++;
        this.queue[index] && this.load(this.queue[index], index);

        return this;
    };

    preLoader.prototype.processQueue = function(){
        // runs through all queued items.
        var i = 0,
            queue = this.queue,
            len = queue.length;

        // process all queue items
        this.reset();

        if (!this.options.pipeline) for (; i < len; ++i) this.load(queue[i], i);
        else this.load(queue[0], 0);

        return this;
    };

    function checkProgress(src, image){
        // intermediate checker for queue remaining. not exported.
        // called on preLoader instance as scope
        var args = [],
            o = this.options;

        // call onProgress
        o.onProgress && src && o.onProgress.call(this, src, image, this.completed.length);

        if (this.completed.length + this.errors.length === this.queue.length){
            args.push(this.completed);
            this.errors.length && args.push(this.errors);
            o.onComplete.apply(this, args);
        }

        return this;
    }


    if (typeof define === 'function' && define.amd){
        // we have an AMD loader.
        define(function(){
            return preLoader;
        });
    }
    else {
        this.preLoader = preLoader;
    }
}).call(this);

This can be in a separate file or as part of your utilities. Use is up to you, the API supports the following:

new preLoader(['image1.jpg', 'image2.jpg'], /*optional*/options);

In a more comprehensive example, it can be used to load multiple images, display a progress indicator (HTML5 browsers) and fire a function when cache is primed.

// assign 50 non-cache-able images via an image generator
var imagesArray = new Array(50).join(',').split(',');
imagesArray = imagesArray.map(function(el, i){
    return 'http://dummyimage.com/600x400/000/' + i + '?' + +new Date();
});

// create a HTML5 progress element
var progress = document.createElement('progress');
progress.setAttribute('max', imagesArray.length);
progress.setAttribute('value', 0);
document.body.appendChild(progress);

var legend = document.createElement('span');
document.body.appendChild(legend);
var imageContainer = document.getElementById('images');

// instantiate the pre-loader with an onProgress and onComplete handler
new preLoader(imagesArray, {
    onProgress: function(img, imageEl, index){
        // fires every time an image is done or errors. 
        // imageEl will be falsy if error
        console.log('just ' +  (!imageEl ? 'failed: ' : 'loaded: ') + img);
        
        var percent = Math.floor((100 / this.queue.length) * this.completed.length);
        
        // update the progress element
        legend.innerHTML = '<span>' + index + ' / ' + this.queue.length + ' ('+percent+'%)</span>';
        progress.value = index;
        
        imageContainer.appendChild(imageEl);
        // can access any propery of this
        console.log(this.completed.length + this.errors.length + ' / ' + this.queue.length + ' done');
    }, 
    onComplete: function(loaded, errors){
        // fires when whole list is done. cache is primed.
        console.log('done', loaded);
        imageContainer.style.display = 'block';
        
        if (errors){
            console.log('the following failed', errors);
        }
    }
});

The preLoader supports two different modes of operation: parallel and pipeline (pass it in the options). In parallel, it just dumps the queue to the browser and lets it manage threads/concurrency on its own. When pipeline: true is passed, it will only use a single thread and won't start downloading the next image until the previous has finished or errored out. For more info, look at the example folder on the repository to see the difference.

parallel waterfall

pipeline waterfall


Viewing all articles
Browse latest Browse all 40

Trending Articles