A better way to preload images for web galleries
The canonical javascript image preloader attempts to maximize the browser’s ability to load resources in parallel. While this is a good strategy for certain resources, it is not necessarily the best strategy for loading a gallery full of high resolution images, especially if you can predict user behavior.
by Mark Meyer
Photographer’s websites are often image-heavy, which can make them unreasonably sluggish, especially for people on mobile or other low-bandwidth connections. Finding the perfect balance between image size, image quality, and download speed is a constant challenge. Professional photographers understand that the viewers who are most important to their business — photo editors, buyers, art directors, etc. — will have fast connections and large screens leading many of us to err on the side of larger, higher-quality files at the expense of speed. But even on a quick connection, waiting a second or two for every image you want to see becomes tiresome. Image preloading solves this problem by downloading the images in a gallery before the viewer has requested them increasing the odds that the images will be cached and instantly available when they are needed.
The standard way, but maybe not the best way
An informal survey on the web (for example) reveals what appears to be a consensus on the canonical way to preload a set of images. In its simplest form it looks something like this:
/* 'images' is an array with image metadata including a 'url' property */
for (var i = 0; i < images.length; ++i) {
var img = new Image();
img.src = images[‘url’];
}
This snippet loops through an array of objects containing image metadata, creates a new HTML Image object for each one, and assigns a URL to its src
attribute. As soon as each image object is assigned a src
value, the browser will fire off a request to the server and cache the image when it is returned.
There’s a subtly here, however, that is not immediately obvious: the browser requests are asynchronous, which means this code will loop through and request every image almost instantly without waiting to hear back from the server. In other words, the browser will try to download as many images as it can at the same time. For modern browsers the means this code will try to download 4-8 images in parallel (or more if they are coming from different domains).
The benefit of parallel downloads
Websites typically have a lot of small parts that the browser must download before it can render the page. There’s the HTML itself, normally a css file or two, some small graphic elements, fonts, and occasionally an unavoidable javascript file that needs to be evaluated before the page can draw. Each one of these files is normally quite small, but each one requires a roundtrip to the server which incurs a latency cost. Although this latency is normally small — measured in milliseconds for each file — if the browser had to wait for each request to finish before firing off the next request, those milliseconds would accumulate sequentially and quickly add up to seconds. These would be additional seconds the viewer would have to wait before they can see anything on the page. If you could instead fire off all the requests simultaneously you would be able to take the latency hit once for the whole group and shave a few seconds off the page download. This doesn’t make the actual download of each file’s data faster — you are still limited to a certain amount of bandwidth so those 4-8 parallel requests are downloading 4-8 times slower — but it does make the total download faster because you avoid the sequential latency cost. Because the browser can’t draw the page until it has all the critical elements, preventing this sequential latency from adding up means a quicker page draw.
This isn’t necessarily a good thing for preloading images
Parallel loading works really well for the initial page elements because the browser needs all of these resources before it can render the page. It doesn’t help if you can get one css file to the browser before another, because it needs both before it can do anything. But this isn’t the case with preloaded images in a gallery. There is a very good chance that you can predict which file will be needed first with enough confidence that you should prioritize it even if it means the total preload takes a little longer.
If you haven’t tested your site on anything other than a broadband connection, you might be surprised to find that your preloader is actually making your site perform worse for many users.
The analytics data from my web galleries is very clear. Although there are a number of ways to navigate from one image to another — thumbnails and previous & next links — 90% of the clicks are on the next image link. In almost every case the most critical element after the page has finished loading is the image file that is next in line in the gallery. If you use the canonical pre-loading code you can’t control when this image loads. The browser will try to load every image in the gallery in the largest groups of concurrent requests it can. This is a good technique to reduce the over all time it takes to load the gallery, but it means that the time to load the most important image, the next one, is significantly longer because it is competing for bandwidth with the other simultaneous requests. On a moderately slow 1.5Mbps DSL connection, a 350k image can take 2 seconds to load. It is possible the viewer will have wait until 4-6 images have loaded before this one is ready. This translates into a potential 12-15 second wait before the next image in the gallery is viewable. The upside is that 4-6 other images will also now be cached and ready, but we’ve likely lost this viewer by making him or her stare at a loading graphic for 12 seconds. If you haven’t tested your site on anything other than a broadband connection, you might be surprised to find that your preloader is actually making your site perform worse for many users.
A better preloader
If you understand the behavior of your viewers, you can design a preloader that will provide a better experience for the large majority of them. Since I know almost all viewers use my gallery sequentially, the best strategy for me is to load the images in that same sequence. The overall time to load all images will be somewhat longer because we’ll incur the latency cost sequentially, but the overall load time is not as critical here as it is with the initial page load because we don’t need all the images to be loaded before the viewer can use the page. We only need the one the viewer wants to see right now. The javascript I’m now using looks like this:
function preload(imageArray, index) {
index = index || 0;
if (imageArray && imageArray.length > index) {
var img = new Image ();
img.onload = function() {
preload(imageArray, index + 1);
}
img.src = images[index][‘serving_url’];
}
/* images is an array with image metadata */
preload(images);
Note: this has been simplified a bit for clarity. The production code sends different sized images to different devices based on their maximum display size and accounts for the case when a user enters the gallery at an image other than the first one.
This takes the first image in the array (at index 0), attaches an onload
handler, and then requests the image. Only after this image has finished loading does it call the onload
handler which then does the same thing for the next image. This continues until all images are loaded.
We can compare the download sequence and times on a simulated 2Mbps connection using Chrome’s development tools:
Parallel preloading
(Simulated 2Mbps connection — total time: 24.01 seconds. larger version )
Sequential preloading
(Simulated 2Mbps connection — total time: 25.44 seconds. larger version )
With the sequential download this image has the full bandwidth to itself and is ready in 1.46 seconds — almost 1000% faster.The canonical preloader shaved 1.5 seconds off the total time. But that’s not the most important statistic in this case. The top line of each chart represents the download time of the next image in the gallery — the one 90% of my users will ask for next. Using the canonical preloader code with a moderately slow connection, that image is not available to our viewer until 14.27 seconds after the page loads even though it was first in line and began downloading immediately after the main page loaded. This is because it is sharing bandwidth with a bunch of other files as you can see in the chart. With the sequential download this image has the full bandwidth to itself and is ready in 1.46 seconds — almost 1000% faster. For our most common use case this is an immense improvement.
One concern with this strategy is what happens with non-typical viewer behavior? What if our viewer is in the ten percent of people who hit the previous-image link or pick a thumbnail to jump around in random order? Because our image loader is only loading one image at a time, it is still possible to load another in parallel without a dramatic performance penalty. The javascript that responds to user requests is able to simply request the image like it normally would. Since it’s probably not already preloaded and cached, the browser will request it alongside whichever image is currently downloading, essentially letting this new request cut in line. Since there is only one other image downloading at a given time, the viewer will only have wait a little longer than normal. In almost every case this results in a much better experience than if they tried to add an additional request on top of the six concurrent requests the browser is already attending to.
An additional benefit is that since the browser is only dealing with one image at a time, the interface remains responsive. With the canonical version, the browser is trying to download and process a half dozen large image files simultaneously, which can chew up CPU time making animated elements on the page suffer until the download is finished.
One note — avoid preloading too soon.
A lot has to happen between the time a user requests your page and the time the browser can render the page. While this is happening your user is sitting there looking at a blank page. If you value this viewer, you will try to make this time as short as possible. To do this you need to deliver the minimum amount of data the browser needs to render the page as quickly as possible. Your preloader should stay out its way. If you stick your image preload code in the head of your document there is a good chance the browser is going to try to satisfy these requests while it it also downloading critical page elements. You can improve on this by adding preload code to the end of the document before the closing
tag, but this still isn’t ideal.The best place, at least for my case, is in the window onload handler. If you are using JQuery that would be here:
$(window).load( function(){
/* Preload code goes here */
});
The reason I’m using $(window).load
rather than $(document).ready
is because I’m loading some images on the initial page load as css background images. These images may not be finished loading when $(document).ready
is called, but they will be when $(window).load
is called. This lets me get the interface and all the initial images to the viewer quickly before we start preloading. The result is that all the navigation, thumbnails, and the single main image are loaded as quickly as possible first before we deal with preloading.