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 methoddone()
. This enablesdone()
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 thepostaction()
function with it. Thanks to the power of closures, our mainpreloadimages()
function has access to the user defined callback functions via thepostaction()
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 thedone()
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!