Preloading Images in Parallel with Promises

Share this article

The topic of this article is actually quite specific. Recently, I faced a situation where I needed to preload a lot of images in parallel. With the given constraints, it ended up being more challenging than first expected, and I certainly learnt a lot along the journey. But first, let me describe the situation shortly before getting started. Let’s say we have a few “decks” on the page. Broadly speaking, a deck is a collection of images. We want to preload the images of each deck, and be able to know when a deck is done loading all its images. At this point, we are free to run any piece of code we want, such as adding a class to the deck, running an image sequence, logging something, whatever… At first, it sounds quite easy. It even sounds very easy. Although perhaps you, like I did, overlooked a detail: we want all decks to load in parallel, not in sequence. In other words, we do not want to load all images from deck 1, then all images from deck 2, then all images from deck 3, and so on. Indeed, it is not ideal because we end up having decks waiting for previous ones to finish. So in a scenario where the first deck has dozens of images, and the second one has only one or two, we would have to wait for the first deck to be fully loaded before being ready for deck 2. Ugh, not great. Surely we can do better! So the idea is to load all the decks in parallel so that when a deck is fully loaded, we don’t have to wait for the others. To do so, the rough gist is to load the first image of all the decks, then the second of all the decks, and so on until all the images have been preloaded. Alright, let’s start with creating some markup so we all agree on what’s going on.
By the way, in this article I will assume that you are familiar with the idea of promises. If it is not the case, I recommend this little reading.

The Markup

Markup-wise, a deck is nothing but an element such as a div, with a deck class so we can target it, and a data-images attribute containing an array of image URLs (as JSON).
<div class="deck" data-images='["...", "...", "..."]'>...</div>
<div class="deck" data-images='["...", "..."]'>...</div>
<div class="deck" data-images='["...", "...", "...", "..."]'>...</div>

Preparing the Ground

On the JavaScript side, this is —unsurprisingly— a bit more complex. We will build two different things: a deck class (please put this between very large quotation marks and do not nitpick over the term), and a preloader tool. Because the preloader has to be aware of all the images from all the decks in order to load them in a specific order, it needs to be shared across all the decks. A deck cannot have its own preloader, else we end up with the initial problem: code is executed sequencially, which is not what we want. So we need a preloader that is being passed to each deck. The latter adds its images to the queue of the preloader, and once all the decks have added their items to the queue, the preloader can start preloading. The execution code snippet would be:
// Instantiate a preloader
var ip = new ImagePreloader();
// Grab all decks from the DOM
var decks = document.querySelectorAll('.deck');

// Iterate over them and instantiate a new deck for each of them, passing the
// preloader to each of them so that the deck can add its images to the queue
Array.prototype.slice.call(decks).forEach(function (deck) {
  new Deck(deck, ip);
});

// Once all decks have added their items to the queue, preload everything
ip.preload();
I hope it makes sense, so far!

Building the Deck

Depending on what you want to do with the deck, the “class” can be quite long. For our scenario, the only thing we do is add a loaded class to the node when its images have done loading. The Deck function has not much to do:
  1. Loading the data (from the data-images attribute)
  2. Appending the data to the end of the preloader’s queue
  3. Telling the preloader what to do when the data has been preloaded
var Deck = function (node, preloader) {
  // We get and parse the data from the `data-images` attribute
  var data = JSON.parse(node.getAttribute('data-images'));

  // We call the `queue` method from the preloader, passing it the data and a
  // callback function
  preloader.queue(data, function () {
    node.classList.add('loaded');
  });
};
It’s going very well so far, isn’t it? The only thing left is the preloader, although it is also the most complex piece of code from this article.

Building the Preloader

We already know that our preloader needs a queue method to add a collection of images to the queue and a preload
method to launch the preloading. It will also need a helper function to preload an image, called preloadImage. Let’s start with that:
var ImagePreloader = function () { ... };
ImagePreloader.prototype.queue = function () { ... }
ImagePreloader.prototype.preloadImage = function () { ... }
ImagePreloader.prototype.preload = function () { ... }
The preloader needs an internal queue property to hold the decks it has to preload, as well as their respective callback.
var ImagePreloader = function () {
  this.items = [];
}
items is an array of objects, where each object has two keys:
  • collection containing the array of image URLs to preload,
  • callback containing the function to execute when the deck is fully loaded.
Knowing this, we can write the queue method.
// Empty function in case no callback is being specified
function noop() {}

ImagePreloader.prototype.queue = function (array, callback) {
  this.items.push({
    collection: array,
    // If no callback, we push a no-op (empty) function
    callback: callback || noop
  });
};
Alright. At this point, each deck can append its images to the queue. We now have to build the preload method that will take care of actually preloading the images. But before jumping on the code, let’s take a step back to understand what we need to do. The idea is not to preload all the images from each deck, one after the other. The idea is to preload the first image of each deck, then the second, then the third, and so on. Preloading an image means creating a new image from JavaScript (using new Image()) and apply a src to it. This will intimate the browser to load the source asynchronously. Because of this asynchronous process, we need to register a promise, that will resolve when the resource has been downloaded by the browser. Basically, we will replace each image URL from our arrays with a promise that will resolve when the given image has been loaded by the browser. At this point, we will be able to use Promise.all(..) to have an ultimate promise that resolves when all the promises from the array have resolved. And this, for each deck. Let’s start with the preloadImage method:
ImagePreloader.prototype.preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    // Create a new image from JavaScript
    var image = new Image();
    // Bind an event listener on the load to call the `resolve` function
    image.onload  = resolve;
    // If the image fails to be downloaded, we don't want the whole system
    // to collapse so we `resolve` instead of `reject`, even on error
    image.onerror = resolve;
    // Apply the path as `src` to the image so that the browser fetches it
    image.src = path;
  });
};
And now, the preload method. It does two things (and thus could possibly be split in two different functions, but that’s outside of the scope for this article):
  1. It replaces all the image URLs with promises in a specific order (first image from each deck, then second, then third…)
  2. For each deck, it registers a promise that calls the callback from the deck when all the promises from the deck have resolved (!)
ImagePreloader.prototype.preload = function () {
  // Promises are not supported, let's leave
  if (!('Promise' in window)) {
    return;
  }

  // Get the length of the biggest deck
  var max = Math.max.apply(Math, this.items.map(function (el) {
    return el.collection.length;
  }));

  // Loop from 0 to the length of the largest deck
  for (var i = 0; i < max; i++) {
    // Iterate over the decks
    this.items.forEach(function (item) {
      // If the deck is over yet, do nothing, else replace the image at
      // current index (i) with a promise that will resolve when the image
      // gets downloaded by the browser.
      if (typeof item.collection[i] !== 'undefined') {
        item.collection[i] = this.preloadImage(item.collection[i])
      }
    }, this);
  }

  // Iterate over the decks
  this.items.forEach(function (item, index) {
    // When all images from the deck have been fetched by the browser
    Promise.all(item.collection)
      // Execute the callback
      .then(function () { item.callback() })
      .catch(function (err) { console.log(err) });
  });
};
That’s it! Not that complex after all, do you agree?

Pushing Things Further

The code is working great although using a callback to tell the preloader what to do when a deck is loaded is not very elegant. You might want to use a Promise rather than a callback, especially since we used Promises all along! I was not sure how to tackle this so I have to agree I asked my friend Valérian Galliat to help me on this. What we use here is a deferred promise. Deferred promises are not part of the native Promise API so we need to polyfill it; thankfully it is only a matter of a couple of lines. Basically a deferred promise is a promise than you can resolve later on. Applying this to our code, it would change very little things. The .queue(..) method first:
ImagePreloader.prototype.queue = function (array) {
  var d = defer();
 
  this.items.push({
    collection: array,
    deferred: d
  });
    
  return d.promise;
};
The resolution in the .preload(..) method:
this.items.forEach(function (item) {
  Promise.all(item.collection)
    .then(function () { item.deferred.resolve() })
    .catch(console.log.bind(console));
  });
And finally the way we add our data to the queue of course!
preloader.queue(data)
  .then(function () {
    node.classList.add('loaded');
  })
  .catch(console.error.bind(console));
And we’re done! In case you want to see the code in action, take a look at the demo below:

See the Pen QjjGaL by SitePoint (@SitePoint) on CodePen.

Conclusions

There you go folks. In about 70 lines of JavaScript, we managed to asynchronously load images from different collections in parallel, and execute some code when a collection is done loading. From there, we can do a lot of things. In my case, the point was to run these images as a quick loop sequence (gif-style) when clicking a button. So I disabled the button during the loading, and enabled it back once a deck was done preloading all its images. Thanks to this, the first loop run is going seamlessly since the browser has already cached all the images. I hope you like it! You can have a look at the code on GitHub or you can play with it directly on CodePen.

Frequently Asked Questions (FAQs) about Preloading Images in Parallel with Promises

What is the significance of preloading images in web development?

Preloading images is a crucial aspect of web development. It enhances the user experience by reducing the load time of web pages. When images are preloaded, they are loaded in the background before they are required. This means that when a user navigates to a page that requires these images, they are already loaded and ready to be displayed, resulting in a smoother and faster browsing experience.

How does preloading images with promises work?

Promises in JavaScript are objects that represent the eventual completion or failure of an asynchronous operation. When preloading images with promises, each image load is represented as a promise. These promises are then run in parallel, meaning they all start loading at the same time. Once all the promises (image loads) are fulfilled, the images are ready to be displayed.

What are the benefits of preloading images in parallel with promises?

Preloading images in parallel with promises can significantly improve the performance of a website. It allows multiple images to be loaded at the same time, reducing the overall load time. Additionally, using promises ensures that the images are loaded correctly and any errors can be handled appropriately.

Can I preload images without using JavaScript?

Yes, it is possible to preload images without using JavaScript. This can be done using HTML’s link element with the rel attribute set to “preload”. However, this method does not provide the same level of control and error handling as preloading with JavaScript and promises.

How does AI relate to preloading images?

AI can be used to enhance the process of preloading images. For instance, AI can analyze user behavior and predict which images a user is likely to view next. These images can then be preloaded, further improving the user experience.

What is the impact of preloading images on SEO?

Preloading images can have a positive impact on SEO. Faster load times lead to a better user experience, which is a factor that search engines consider when ranking websites. Therefore, preloading images can potentially improve a website’s search engine ranking.

How can I handle errors when preloading images with promises?

Promises in JavaScript provide built-in error handling. If an error occurs while loading an image, the promise will be rejected and the error can be caught and handled appropriately.

Can I control the order in which images are preloaded?

When preloading images in parallel with promises, all images start loading at the same time. However, it is possible to control the order in which the images are displayed once they are loaded.

Is preloading images with promises supported in all browsers?

Most modern browsers support promises in JavaScript, and therefore support preloading images with promises. However, it’s always a good idea to check the specific browser compatibility when implementing this technique.

Are there any downsides to preloading images?

While preloading images can improve the user experience by reducing load times, it can also consume more bandwidth. This could potentially be an issue for users with limited data plans. Therefore, it’s important to strike a balance between performance and data usage when preloading images.

Kitty GiraudelKitty Giraudel
View Author

Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/they.

AurelioDimagesJavaScriptperformancePreloadPromises
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week