January 21, 2023
A common performance optimisation that I see often recommended when making multiple async calls in javascript is to wrap all asynchronous calls made within Promise.all
For example, say when a user lands on a product page for your app you want to make a call to fetch the productDetails
for the main product and another to call to fetch the recommendedProducts
to show some alternatives and you naively fetch them like so:
const ProductPage = async ({productId}: Props) => {
const productDetails = (await getProductDetails(productId)) ?? {};
const recommendedProducts = (await getRecommendedProducts(productId)) ?? [];
return (<>/*render page*/</>)
}
This is un-optimal as you're fetching the data in series meaning that the time taken to fetch all of the data is the total sum that each request takes
total time = request 1 + request 2
Since each fetch doesn't rely on each other and only relies on the productId
, they can actually be called concurrently which makes the total time required to fetch all of the data just the time it took for whichever request took the longest
total time = max(request 1, request 2)
We can make the requests concurrently by using Promise.all
:
const [productDetails, recommendedProducts] = await Promise.all([
getProductDetails(productId),
getRecommendedProducts(productId)
]);
However this optimisation introduces a subtle change in behaviour when one of the requests fail, if either getProductDetails
or getRecommendedProducts
fail then Promise.all
will fail in turn. This means that if you still want to render the recommended products even if the product details request fails (or vice versa), this approach doesn't currently support it as everything is rejected by Promise.all
Here's a code snippet that you can chuck into dev tools to see what I mean:
const failedFunction = async () => {
let result;
try {
result = await Promise.all([Promise.reject('one'), Promise.resolve('two')])
} catch (e) {
console.log('caught error: ', e)
}
console.log('result: ', result)
}
await failedFunction()
A simple workaround for this is to use Promise.allSettled instead. With this, the result of both requests are saved and the failure of one no longer prevents the other request from resolving.
const halfFailedFunction = async () => {
let result;
try {
result = await Promise.allSettled([Promise.reject('one'), Promise.resolve('two')])
} catch (e) {
console.log('caught error: ', e)
}
console.log('result: ', result)
}
await halfFailedFunction()