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 HTML5link 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:
- It needs to be threaded in parallel (to use the browser’s maximum threads, normally 6 per domain)
- It needs to also be sequential (pipelined) for when you want to use a single thread
- It needs to handle individual image events –
onload
,onabort
,onerror
- Work cross-browser and without any dependencies
- 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