Categories:

Preloading images and executing code only after all images have loaded

Date created: May 23rd, 2011

Preloading images is one of those time tested JavaScript techniques that remain popular even today for loading up images as soon as possible in the background. The idea is to kick start the process as soon as the page begins to load so that when the application in question needs to display them, such as an image rollover effect, those images will hopefully have preloaded already and be shown instantly. The following simple function preloads any number of images when called:

function preloadimages(arr){
	var newimages=[]
	var arr=(typeof arr!="object")? [arr] : arr //force arr parameter to always be an array
	for (var i=0; i<arr.length; i++){
		newimages[i]=new Image()
		newimages[i].src=arr[i]
	}
}

//preload 3 images:
preloadimages(['1.gif', '2.gif', '3.gif'])

For the most part this technique suffices in giving our desired images a head start in terms of their loading, as that's all we're looking for, a head start. Sometimes though, we need more than that- we need an actual guarantee that our images have preloaded fully before accessing them. Consider a photo slideshow where each image should be centered within the outer container when shown. To do this we need access to each image's dimensions to properly orient them, which can only be done reliably when the image has completely loaded. Is it the end of the world if the images aren't always centered? No, but it certainly does ruin the effect we so painstakingly added to differentiate our slideshow from the others. Luckily getting JavaScript to acknowledge when a group of images have actually preloaded isn't difficult. The trick is to utilize the onload and onerror event handlers of JavaScript's image object to detect when an image has fully loaded (or failed at that), then run the callback function when everything is complete. In this article we'll see how to transform our original preloadimages() function into this:

preloadimages(['1.gif', '2.gif', '3.gif']).done(function(images){
 //code inside here is run after all images have preloaded
 //images parameter is an array of image objects corresponding to the preloaded images
 //images[0] references the 1st image etc
})

Lets break up the explanation into two parts now.

Detecting when images have loaded (or failed to load) using the image object's onload and onerror event handlers

To detect when an image has fully loaded or failed to load, we turn to the onload and onerror event handlers of the image object. And by incrementing a counter whenever that happens, we can in turn find out when all images within our collection of images have loaded. Lets modify our original preloadimages() function now to alert a message when it's done with the preloading:

function preloadimages(arr){
	var newimages=[], loadedimages=0
	var arr=(typeof arr!="object")? [arr] : arr
	function imageloadpost(){
		loadedimages++
		if (loadedimages==arr.length){
			alert("All images have loaded (or died trying)!")
		}
	}
	for (var i=0; i<arr.length; i++){
		newimages[i]=new Image()
		newimages[i].src=arr[i]
		newimages[i].onload=function(){
			imageloadpost()
		}
		newimages[i].onerror=function(){
		imageloadpost()
		}
	}
}

//sample run
preloadimages(['1.gif', '2.gif', '3.gif'])

Our function now alerts a message when all of the images passed into it have finished preloading. It does this by incrementing the variable loadedimages each time an image has either loaded or failed to load, and when that number equals the total number of images entered into the function, we know the entire process is complete.

Adding callback functionality to our preloadimages() function

Ok, now that we have the first half of our ultimate goal completed, time to move on to the second part of modifying preloadimages() so it accepts a callback function that kicks in when all the images have preloaded. The classic interface for this is just to have the primary function accept an anonymous function as a parameter, with the later being the callback function:

preloadimages(imagesarray, function(){
 //code to run when images have loaded
})

In this case though, lets do things a little differently. Instead of the above, we'll implement the following interface instead, which arguably is more intuitive from the user's perspective:

preloadimages(imagesarray).done(function(){
 //code to run when images have loaded
})

In other words, we chain another function done() to our main function to be the recipient of any callback code. With that in mind, here is the final modified preloadimages() function first, then a step by step explanation on the changes:

function preloadimages(arr){
	var newimages=[], loadedimages=0
	var postaction=function(){}
	var arr=(typeof arr!="object")? [arr] : arr
	function imageloadpost(){
		loadedimages++
		if (loadedimages==arr.length){
			postaction(newimages) //call postaction and pass in newimages array as parameter
		}
	}
	for (var i=0; i<arr.length; i++){
		newimages[i]=new Image()
		newimages[i].src=arr[i]
		newimages[i].onload=function(){
			imageloadpost()
		}
		newimages[i].onerror=function(){
			imageloadpost()
		}
	}
	return { //return blank object with done() method
		done:function(f){
			postaction=f || postaction //remember user defined callback functions to be called when images load
		}
	}
}

preloadimages(['1.gif', '2.gif', '3.gif']).done(function(images){
 //call back codes, for example:
 alert(images.length) //alerts 3
 alert(images[0].src+" "+images[0].width) //alerts '1.gif 220'
})

Lets go over the changes now:

  • First, I define an empty postaction() function that will be used later on to store any user defined callback functions that should be run when the images are fully preloaded.

  • At the end of  preloadimages() it returns an empty object containing a simple method done(). This enables done() to be chained on top of the main function, with its purpose being to accept and preserve the callback functions defined by the user, by wrapping them inside an anonymous function and then overwriting the postaction() function with it. Thanks to the power of closures, our main preloadimages() function has access to the user defined callback functions via the postaction() function.

  • Note how the anonymous function itself is indirectly passed the newimages array so the user has access to all of the preloaded images as an array of image objects inside the done() method.

In the below we'll use our newly constructed function to preload some images then sort them based on their widths (ascending):

preloadimages(['ed.jpg', 'fei.jpg', 'budapest.gif', 'duck.jpg']).done(function(images){
	images.sort(function(a,b){
		return a.width-b.width //sort images by each image's width property, ascending
	})
	alert(images[0].src) //alerts the src of the smallest image width wise
})

Cool!