devxlogo

Develop a Reusable Image Cache in JavaScript

Develop a Reusable Image Cache in JavaScript

cripting a browser’s Document Object Model may be easy these days, but while the DOM is highly functional in some areas, it remains distinctly inflexible in others. Asynchronous operations like image loading have always been a gray area, with plenty of useful events exposed by the DOM, but little advice on how to utilize them to full effect. Knowing when a block of images has loaded is often critical to achieving the page layout, functionality, and interactivity you require. In this article I’ll explain how I developed a few simple classes to do just that, thereby making image-loading headaches a thing of the past.

Open up any Web site authored with Macromedia’s Dreamweaver, and chances are you’ll see a bit of code at the top of the page that looks like this:

   function MM_preloadImages()    {       //v3.0      var d=document;       if(d.images)      {          if(!d.MM_p)            d.MM_p=new Array();             var i,j=d.MM_p.length,a=MM_preloadImages.arguments;          for(i=0; I

Actually, it probably won't look quite as structured, since the whole lot's compacted into five lines, but you get the general idea. A little later you may well see an onload handler that looks something like this:

   

The purpose of this code is to "preload" a set of images?typically onmouseover and onmouseout rollovers. If the images weren't preloaded, you would notice a perceptible delay the first time you moved the mouse over a rollover image before the rollover image would display, as the browser would have to go off and fetch it from the server then and there. In fact, "preloading" is a bit of a misnomer, as code like this makes no guarantees that it will have finished loading all the rollovers before you need to access them; it simply kicks off loading them when the document itself is ready.

With a slow connection, an unresponsive server, or graphics with a large footprint, your mouse might get there before the highlights do. For most purposes this is an acceptable risk. However, it does bring up a couple of important issues, namely: How can you determine if and when an image or set of images has been downloaded; and how should you structure the workflow of any scripts that need to manipulate them? To answer these questions, we need first to take a closer look at how the browser's DOM handles the interaction between image loading and script execution. Note that, while this article's code is specific to Internet Explorer, the principles behind the loading pipeline and its threading issues are applicable to any browser.

Images, the Loading Pipeline, and the DOM
When your Web browser opens an HTML page, it parses the document for IMG tags and starts to load them asynchronously while simultaneously running any JavaScript it's been asked to execute. Loading images isn't the only thing that happens asynchronously; XML data sources, ActiveX Controls, and even the document itself pass through a series of compositional stages commencing with uninitialized and culminating in complete?meaning that everything's been loaded, wired up, and is ready to go. Each time an object moves on to the next stage, it fires an onreadystatechange event, and at any time you can also check its readyState property to determine which loading stage it's currently at.

There's a good argument for why things are the way they are. Why bother to download images one by one and put your browser at the mercy of a single, slow server, when you can download them all at once? And because images take up so much bandwidth, why not do something productive while you wait for them?like get on with processing the rest of the page?

Unfortunately, the DOM's asynchronous loading model doesn't collaborate too well with JavaScript's single-threaded execution model, meaning that it can be difficult to determine if and when all the images on the page have finished loading.

This is important because it can have significant repercussions for the workflow of any scripts on the page. For example, suppose you wanted to write a slideshow that loaded source images of varying sizes and displayed them in a box on the screen. In order to preserve the aspect ratio of each image, you would obviously need to know its dimensions before you attempted to display it. But until an image has at least partially loaded, the DOM doesn't know much about it, and this means you can't retrieve a meaningful width or height for it. Given that you can kick off loading an image with code like:

   function LoadImages()   {      var im = new Image();      im.src = "_images/A1.jpg";   }

you might consider preloading the slideshow's image set with something like this:

   // Load some images   LoadImages();   // When the images have loaded, do something else   DoSomethingElse();

Unfortunately, you can't. Because the DOM loads asynchronously, DoSomethingElse() in the code above would get called before LoadImages() had completed. "Aha!" you might think. "I can solve that problem by getting each image to set a flag when it's finished and then poll for that flag," using code like:

   // Load the first image   var im = new Image();   im.src = "_images/A1.jpg";   // When it's loaded, do something else   while (im.readyState != "complete")   {      // Just loop round, doing nothing   }   // Now do something else   DoSomethingElse();

This won't work either, but for a different reason. Once execution enters the polling loop, it never leaves it, and consequently the thread has no spare cycles with which to load the very image it's waiting for. im.readyState will never complete; in fact, since the browser runs as a priority process, your entire system could well lock up for some time.

I have seen many other ingenious attempts to get around JavaScript's threading problems; some, involving timers, may work partially. But all this cleverness really just evades the issue at hand, which is that the browser's DOM isn't designed to be used this way. The "correct" way to handle asynchronous activities (like loading images) is to respond to the various completion events they raise, and to structure your scripts so that their workflow is driven by these events, not the other way round.

Image Caching and Image Groups
To make things easier, I decided to develop a more general framework for handling images. In doing so, I wanted to encompass more than just "preloading" them (a vague term, at best). Instead, I tried to decide what users commonly want to do with images and then develop an API that would satisfy those needs, while making it as easy as possible to work around the fact that images don't load synchronously. My conclusion was that?so far as synchronized loading goes?images belong in groups: navigation groups, icon groups, content groups, rollover groups. Sometimes the groups that interest us are even larger?for example, the set of images on the current page or on the Web site as a whole?but all that really concerns us is that the group of images in question has or has not completed loading. We then use this information to scale related graphics to common sizes, align thumbnails, relatively displace document elements, or simply load and display images together as a set.

All you have to do is put any code you want to execute after your set of images has loaded into a separate method, and then specify this method as the group's completion handler.

The initial API I came up with was a single, self-contained script that used a few globals to control the interactions between two collaborative objects. Here's how it works. A top level ImageCache holds all the images you've ever tried to load since it was instantiated, and you interact with it by creating an ImageGroup, adding the set of images you want to load to it, and then asking the cache to go off and get them. The cache then checks if any of the requested images have already been loaded, and, if not, starts to load them. Finally, when all the images are available, it executes a callback of your choice so that you can do something with the set as a whole.

What's good about it is it's generic; you use it the same way whether you want to load one image or 100. The callback may seem a bit of a kludge, but it's the only sensible way to get around the threading issues we've just covered. It also means it's very easy to adapt your code to achieve the semblance of synchronicity you wanted all along. All you have to do is put any code you want to execute after your set of images has loaded into a separate method, and then specify this method as the group's completion handler.

For example, to return to the slideshow scenario I discussed earlier, here's how to code LoadImages() so that DoSomethingElse() only runs when all the images have finished loading:

   function LoadImages()   {      // Create an ImageGroup and add any images to load      var group = new ImageGroup();      group.AddImage("_images/A1.jpg");      group.AddImage("_images/A2.jpg");      ...      // Set a completion handler      group.OnComplete = "javascript:DoSomethingElse()";      // Load the group      g_image_cache.LoadGroup(group);   }

Strictly speaking, you don't need an image cache if all you want is to be notified when a set of images has loaded. Without it, though, any code that tries to load an image that has already been loaded by another piece of code would end up creating a separate image resource each time, which is wasteful. It would then go ahead and load the image all over again, which would be even sillier!

As it is, the ImageGroup and ImageCache objects have an efficient relationship: The ImageCache ensures no image is loaded more than once, and ImageGroup objects make sure to return cached images if they are available. Consequently, after the initial loading's been done, successive calls to the LoadImages() method above simply result in DoSomethingElse() getting called straight away.

Providing Feedback
Though we've simplified the job of getting a particular asynchronous task to interact with synchronous ones in a consistent way, the fact remains that the "old" way has its advantages. Most importantly, you get constant feedback on the loading process, meaning that, while you might have to wait a while for large images to download, at least you can see that they're progressing.

Because JavaScript can't raise its own custom events (unless it's a scriptlet), there are two obvious ways you can mimic this behaviour using my API as its stands:

  • Have the loading process proactively "push" its state of completion into the document, in the form of well-defined properties; or
  • Images fire onreadystatechange events as they load. Handle these events and try to relate the information back to the progress of some particular set of images you asked to load

The first solution isn't the most elegant, but in most practical cases it's probably the easiest way to get the effect you want. Suppose for example the API was bound to some document element called progressElement, which exposed a property called width. Without further ado you could get instantaneous graphical feedback on the group you were loading simply by dropping in a bit of HTML code like this:

   or   

As the images loaded, document.progressElement.width would get updated, causing the loader line to widen accordingly. Ultimately, though, I had to reject this solution on account of its inflexibility. If the loader line's width was specified in pixels, you would need some way to tell the API the maximum width it should return, in order to accommodate different length "progress bars."

But maybe you wouldn't want a progress bar after all; maybe you would prefer to display the percentage complete as a string? Or perhaps you don't want to display anything at all; you just want to know how many images in the current group have loaded, and how many are left to load? Each case would require the API to expose a separate document property?a classic example of the potential pitfalls you can expect when you tightly bind controls without sufficient forethought.

The second solution also has its drawbacks. We would have to be careful only to catch state change events that originated from images preloaded by the image cache, so we didn't interfere with any other asynchronous elements. At the same time, for reasons of data abstraction, it would be nicer not to have to poke around with global variables to find out the information we want.

In fact, we can get both of these effects, and even something approaching the "custom event" we wanted all along, by adding an additional layer of abstraction:

  • Hook the readystatechange event for each image loaded via an ImageGroup object
  • Each time the hook gets invoked, populate some kind of "progress" object with appropriate data, and pass it as a parameter to a user-defined update handler (if specified). Because an ImageGroup already knows which images need to be loaded and how many have loaded to date, it makes sense simply to pass this as our "progress" object.

This way, you effectively adapt (i.e. wrap) an in-built document event (readystatechange) to create the illusion of a custom event (imageupdate) with its own originator (an ImageProgress object), which you can hook up in exactly the same way as any other document event. Assuming you're interested in the event, you assign it to a handler of your choice, within which you can script the document in any way you see fit.

This is a much better model than the initial one discussed in this article, and since you can now establish loading progress within the imageupdate handler, ImageGroup no longer needs its OnComplete() delegate; you can just call its IsComplete() method to see when it's finished.

To get maximum usefulness out of the API though you also need to provide a way to retrieve images directly from the cache, so that scripts with no direct involvement in loading images themselves can search for them and manipulate them if they exist. The GetImage() method, exposed on the ImageCache object, does just this. You'll see it used in the setImages() method of auto-load.js to iterate through the set of images on the page and display them all together once they've finished loading.

Finally, in order to dissuade you from working directly with g_image_cache, the global that holds an instance of ImageCache, I've wrapped and re-exposed the cache's two salient methods, LoadGroup() and GetImage(), at the top level. This doesn't mean you can't manipulate the cache directly if you want, but hopefully it'll encourage you to think twice before doing so frivolously. Listing 1 presents the final code for load-image.js.

Applying the Image Cache API
Theory is all very well, but the proof is in the pudding. So let's move on to some concrete applications.

If all you're after is a facility to preload a page's static image content and then display it (like the Dreamweaver code we started with), you can add a little code to do this programmatically (see Listing 2). autoload.js exposes two methods. The first, loadImages(), creates and loads an ImageGroup after populating it with the name attribute of each IMG tag in the document. Then setImages() reconnects the newly loaded images with the document.

If you want the images to preload as soon as you open the page, all you need to do is include the relevant script files and hook the document's onload event; everything else happens automatically. This means your resulting HTML can be as simple as this:

                                       		                        ... etc.         

If you unzip the demo project that accompanies this article and run preload.htm, you can see auto-load.js in action. Usually, you would hook up loadImages() as above, but in preload.htm I've added a button to do this explicitly.

You might also want to take a look at slideshow.htm (see download, left column), which implements the hypothetical slideshow we discussed earlier, providing feedback with a progress bar and rescaling images on the fly so that they fit within the bounds of the viewing box. After loadSlides() kicks off a group of images to load (pictures of Warkworth's A&P Festival, to be precise), execution first loops around updateProgress(), increasing the length of the progress line as the various images complete their loading, and subsequently around updateSlide(), which cycles through the group's image set at two-second intervals.

Ultimately the ability to control how and when images load can be a powerful asset. For example, if you embedded the ImageCache in a hidden, top-level frame, it could take responsibility for all the images on a Web site. You could even set a timer so that, while you were viewing one page, it would be secretly preloading the images for the next. Because image data contributes more to the bulk of most HTML pages than any other factor, this could have a significantly beneficial effect on the perceived loading and rendering time of the site as a whole.

The techniques I've presented could easily be extended to provide caching support for other asynchronous operations within the DOM, such as preloading XML documents or collections of HTML pages associated with other frames. Half the battle is learning to respect the DOM's event-driven model. Once you do you'll realize how little additional code you need to get event handlers and code coexisting in a meaningful workflow rather than working in conflict.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist