Az

JavaScript Event Loop: Part 2

I gave an intro to the JavaScript event loop and how it can be useful nearly 6 years ago and wanted to bring up a quick optimisation that I’ve run into a few times recently regarding handling a bunch of independent asynchronous calls more efficiently.

A common scenario you’ll probably discover in frontend JavaScript or backend Node.js is the need to run a bunch of asynchronous functions. A typical form of this might be getting the results from a few different APIs or downloading a bunch of files from a remote server. Below you’ll find two potential solutions to this common problem:

const asyncFunction = (value) => {
    return new Promise((resolve) => {
        setTimeout(() => resolve(value), 1000);
    });
};

// An array of elements 0 to 8.
const arr = Array.from(Array(10).keys());

async function testWithForOf() {
    let elements = []
    const time0 = Date.now()
    for (const value of arr) {
        const test = await asyncFunction(value);
        elements.push(test);
    }
    console.log(`for...of took: ${(Date.now() - time0) / 1000} seconds`);
}


async function testWithMap() {
    const time1 = Date.now()
    let elements = await Promise.all(arr.map(asyncFunction));
    console.log(`Promise.map() took: ${(Date.now() - time1) / 1000} seconds`);
}

testWithForOf();
testWithMap();

In this test setup, asyncFunction(value) represents any asynchronous function you need to run, taking approximately one second to return a response. If you run this code, you should discover that testWithMap() returns in just over a second while testWithForOf() takes over ten seconds. In both cases we are await-ing the results - expecting all the asynchronous operations to finish before we carry on - but the advantage to testWithMap() is that we’re using the JavaScript event loop to run the asynchronous operations concurrently.

It’s important to note that this example is very simplified, you might not see something like exactly like this in your code. But there are a variety of scenarios where you’re making a bunch of asynchronous calls which may be at least partially independent of each other.

Using Results From Async Function

Maybe you need to use the results from each API call to build a final result where the order of the finished calls is still important, but the calls themselves are independent of each other. Below I’ve put together an example where we complete all our asynchronous calls and then process all the results in order.

const asyncFunction = (value) => {
    return new Promise((resolve) => {
        setTimeout(() => resolve(value), 1000);
    });
};

// An array of elements 0 to 8.
const arr = Array.from(Array(10).keys());

const elements = await Promise.all(arr.map(asyncFunction));
const result = elements.reduce((acc, e) => {
    return acc + e;
}, 0);

Independent Async Functions

Another common setup is when you’ve got a few separate functions you need to run that are all asynchronous and you want to use the results of them. Something like the code below will execute all the functions asynchronously and then await all the results to come back in an array indexed the same order as the functions.

const asyncFunction1 = () => {
    return new Promise((resolve) => {
        setTimeout(() => resolve('a cool result'), 1000);
    });
};

const asyncFunction2 = () => {
    return new Promise((resolve) => {
        setTimeout(() => resolve('another cool result'), 1100);
    });
};

const arr = [asyncFunction1, asyncFunction2];

const results = await Promise.all(arr.map(e => e()));

Connection Limits

If you’re doing any kind of billed by the millisecond ‘serverless’ functions this kind of setup may help shave some money off your bill by reducing the amount of time you spend waiting for operations to finish. One potential issue if you’re doing more than a hundred iterations is your vendor might have limits on the number of simultaneous outogoing connection you have. In this case, you can find a middle ground by “chunking” your array into sequential batches that the platform will allow and then running those chunks asynchronously.

const arr = Array.from(Array(10).keys());
const results = [];
const chunkSize = 2;
for (let i = 0; i < transformed.length; i += chunkSize) {
    console.log(`Chunk: ${i}`);
    const chunk = arr.slice(i, i + chunkSize);
    results.concat(...(await Promise.all(chunk.map(asyncFunction))));
}