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 adiv
, 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 aloaded
class to the node when its images have done loading.
The Deck
function has not much to do:
- Loading the data (from the
data-images
attribute) - Appending the data to the end of the preloader’s queue
- 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 aqueue
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.
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):
- It replaces all the image URLs with promises in a specific order (first image from each deck, then second, then third…)
- 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.
Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/her.