You Should Use Parallel Promises
handling promises in parallel | performance gains | the Promise.all() method.
Asynchronous JavaScript was introduced to help implement non-blocking code execution. Async-await took it up a notch by making it easier to work with promises and avoid the infamous callback hell.
// log every second -> callback hell version
setTimeout(() => {
console.log('1 second');
setTimeout(() => {
console.log('2 seconds');
setTimeout(() => {
console.log('3 seconds');
setTimeout(() => {
console.log('4 seconds');
setTimeout(() => {
console.log('5 seconds');
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
/*
OUTPUT:
1 second
2 seconds
3 seconds
4 seconds
5 seconds
*/
// log every second -> async-await version
const wait = function(time) {
return new Promise(resolve => setTimeout(resolve, time * 1000))
}
async function clock() {
try {
await wait(1);
console.log('1 second');
await wait(1);
console.log('2 seconds');
await wait(1);
console.log('3 seconds');
await wait(1);
console.log('4 seconds');
await wait(1);
console.log('5 seconds');
} catch (err) {
console.log(err);
}
}
clock();
/*
OUTPUT:
1 second
2 seconds
3 seconds
4 seconds
5 seconds
*/
However, one has to be a bit cautious while using await
because it blocks code i.e when await
is used, code execution stops until the promise that it is waiting for is settled (either resolved or rejected). This is different from the .then
and .catch
methods that were used before it, where code execution would not pause. Regular code execution would continue and .then
or .catch
would be called only when the promise had settled.
When you have multiple independent await
statements in your code block, all those statements become blocking statements, potentially breaking the async nature and the core advantage of using async-await in the first place.
Real-world example
I'm loading three images from a remote server using the fetch
method. Since fetch is async and I like modern JavaScript, I take the async-await approach for all asynchronous operations. I then load these images onto the DOM. I also implement a fairly basic timer that works using Date
to get a rough estimate of how much it takes to finish the entire operation.
Here's the code:
const imgContainer = document.querySelector('.img-container');
// simple timer code
let time = '';
function resetTime() {
time = new Date();
}
function elapsedTime(type) {
const now = new Date();
console.log(type, (now - time) / 1000);
}
// fetch photo from server
async function fetchImage(w = 200, h = 300) {
try {
const data = await fetch(`https://picsum.photos/${w}/${h}/`);
return data.url;
} catch (err) {
return new Error(err);
}
}
// load photos to DOM
function loadImages(...sources) {
sources.forEach((source) => {
const img = document.createElement('img');
img.src = source;
imgContainer.append(img);
});
}
(async function () {
resetTime();
try {
const photo1 = await fetchImage(700, 800);
const photo2 = await fetchImage(50, 50);
const photo3 = await fetchImage(100, 200);
loadImages(photo1, photo2, photo3);
} catch (err) {
console.log(err);
}
elapsedTime('one-by-one');
})();
I've embedded a sandbox below that runs the code snippet above. Make note of the time it takes to finish the operation. It took between 2 and 3 seconds for me.
So what's the problem here?
The three await
statements block code execution at each step, making the images get fetched only one after the other. However, the three fetchImage
calls can actually be made independently of one another since fetch was designed to be asynchronous. The only requirement for us is that all three promises should settle before loadImages
is called so that valid values can be passed in as the arguments.
So, by using multiple await
statements one after the other, what was supposed to be an async load operation has now become a synchronous, time-consuming one.
This can also be verified by inspecting the Network tab in the Dev Tools when running the code locally. Notice how the activities only happen sequentially.
If the loadImage
call is temporarily commented out and the await
keyword is removed, you can observe the three fetch statements happening asynchronously.
(async function () {
resetTime();
try {
const photo1 = fetchImage(700, 800);
const photo2 = fetchImage(50, 50);
const photo3 = fetchImage(100, 200);
// loadImages(photo1, photo2, photo3);
} catch (err) {
console.log(err);
}
elapsedTime('one-by-one');
})();
Enter Promise.all()
By now, I hope it is established that having multiple independent await
statements is not a good idea for performance. The solution to this problem is to use the Promise.all()
method
It takes an iterable of promises as input and returns a single promise. An array of promises is usually passed in as the iterable. The returned promise resolves when all the input promises get fulfilled. It rejects when any one of the input promises reject.
Here's how we modify our existing code
Omit the
await
keyword from the threefetchImage()
calls so thatphoto1
,photo2
, andphoto3
become simple promises that will get a resolved value at a future time.Call the
Promise.all()
method and pass in the three promises as an arrayHandle the settled value of this promise.
const photo1 = fetchImage(700, 800);
const photo2 = fetchImage(50, 50);
const photo3 = fetchImage(100, 200);
const allPromises = await Promise.all([photo1, photo2, photo3]);
If allPromises
gets fulfilled, the fulfilled value would be an array, with each element being the resolved value of the corresponding input promise. So in our case, the resolved value will be an array of image URLs.
Here's the full code
(async function () {
resetTime();
try {
const photo1 = fetchImage(700, 800);
const photo2 = fetchImage(50, 50);
const photo3 = fetchImage(100, 200);
const allPromises = await Promise.all([photo1, photo2, photo3]);
} catch (err) {
console.log(err);
}
elapsedTime('parallel');
})();
Here's the sandbox with the updated code
Here's how the network tab looks like when I run the code locally
I tested both versions of the code locally - with and without parallel promises, and checked how long they took. Parallel promises consistently gave significantly better loading times, and were almost 50% faster.
When Not To Use
Although Promise.all()
looks like a one-stop solution for performance improvements, it is important to note that this will only be achieved when used on independent promises.
Consider the case where a fetch request is used to receive a JSON response.
const fetchResponse = await fetch('randomJSONapi');
const jsonData = await fetchResponse.json();
Even though two await
statements are used one after the other, here it is required that the first await
gets settled before the second one is called.
Wrapping Things Up
Returning promises in parallel is definitely a great way to improve code performance. A similar method Promise.allSettled()
has been introduced as part of ES2022, with the difference being that allSettled()
will not reject if one of the input promises reject. Instead, it will return the fulfilled values of the resolved input promises.
I hope you got to learn something new today. Do let me know what you think as a comment or connect with me on Twitter. I'd love to chat!