Awaiting Loops in Node JS along with Increased Performance (Part 1)

Blocking the loop execution

A lot of Node JS developers use forEach loops to operate on Array of elements or on a Map of elements. However, since these loops are non-blocking – they cannot be awaited.

In other words, the process does not wait for execution of these loops to be completed before proceeding to the next line of code. Such a behavior of Node JS can prove to be problematic if the resultant output of the operations performed within the loops is being used in those next lines of code.

For example,

async function normalForEachExample() {
    let xarr = []
    let yarr = [1, 2, 3, 4, 5]
    yarr.forEach(async y => {
        console.info('Pushing ' + y)
        xarr.push("String " + y.toString())
    })
    return Promise.resolve(xarr)
}
async function main() {
    let result = await normalForEachExample()
    console.log(result)
}
main()
</pre>

Since the operations inside the given forEach loop aren’t time consuming, the output that we get is quite straightforward and predictable –

Pushing 1
Pushing 2
Pushing 3
Pushing 4
Pushing 5
[ 'String 1', 'String 2', 'String 3', 'String 4', 'String 5' ]

However, due to the due to the non-blocking nature of forEach loops, the output changes when the process inside the loop is time consuming!

To illustrate it,let us first consider a function that can delay the execution by specified number of milliseconds —

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

We will be using this function, to simulate a delay of 5 seconds in the code. Following is the code that will illustrate the behavior of forEach loop when encountered with a time consuming function.

async function delayedForEachExample() {
    let xarr = []
    let yarr = [1, 2, 3, 4, 5]
    yarr.forEach(async y => {
        await timeout(5000)
        console.info('Pushing ' + y)
        xarr.push("String " + y.toString())
    })
    return Promise.resolve(xarr)
}
async function main() {
     let result = await delayedForEachExample()
     console.log(result)
}
main()

And the output of this code is quite different from the one that is expected! –

[]
Pushing 1
Pushing 2
Pushing 3
Pushing 4
Pushing 5

As it can be seen from the output that due to the time required for it’s execution, forEach is executed after other parts of the code. And the array obtained as a result is thus blank. Imagine what a catastrophe it would be if this array were to have many applications in the code after it !

This problem can however be solved by the use of good old for loops !

Let’s take a look at the example of a delayed loop using for expression!

async function delayedForLoopExample() {
    let xarr = []
    let yarr = [1, 2, 3, 4, 5]
    for (let i = 0; i < 5; i++) {
        await timeout(5000)
        console.info('Pushing ' + yarr[i])
        xarr.push("String " + yarr[i].toString())
    }
    return Promise.resolve(xarr)
}
async function main(){
    let result = await delayedForLoopExample()
    console.log(result)
}
main()

And Voila! The output we get from this is the one we expect 😉

Pushing 1
Pushing 2
Pushing 3
Pushing 4
Pushing 5
[ 'String 1', 'String 2', 'String 3', 'String 4', 'String 5' ]

However, execution time analysis for both the examples (i.e. for foreach and for )reveals that for loop performs each iteration in sequence and is thus slower than the forEach loop which performs the iterations parallely!

Execution Time Analysis :

I will be using process.hrtime() to analyse the time taken for execution of the examples. The Code is as follows –

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
    }
    
async function delayedForEachExample(t) {
    let xarr = []
    let yarr = [1, 2, 3, 4, 5]
    yarr.forEach(async y => {
        await timeout(5000)
        let hrend = process.hrtime(t)
        console.info('Pushing ' + y + ' at : %ds %dms', hrend[0], hrend[1] / 1000000)
        xarr.push("String " + y.toString())
    })
    return Promise.resolve(xarr)
}
async function delayedForLoopExample(t) {
    let xarr = []
    let yarr = [1, 2, 3, 4, 5]
    for (let i = 0; i < 5; i++) {
        await timeout(5000)
        let hrend = process.hrtime(t)
        console.info('Pushing ' + yarr[i] + ' at : %ds %dms', hrend[0], hrend[1] / 1000000)
        xarr.push("String " + yarr[i].toString())
    }
    return Promise.resolve(xarr)
}
async function main() {
    console.log("\nExecution Times for Delayed `For` Loop ---- ")
    var hrStartForLoop = process.hrtime()
    console.log(await delayedForLoopExample(hrStartForLoop))
    let hrEndForLoop = process.hrtime(hrStartForLoop)
    console.info('Total Execution Time for `For loop` : %ds %dms', hrEndForLoop[0], hrEndForLoop[1] / 1000000)
    console.log("\nExecution Times for Delayed `ForEach` Loop ---- ")
    var hrStartForEach = process.hrtime()
    console.log(await delayedForEachExample(hrStartForEach))
}
main()

The results of this analysis are –

Execution Times for Delayed For Loop ----
Pushing 1 at : 5s 0.947358ms
Pushing 2 at : 10s 1.320532ms
Pushing 3 at : 15s 1.166395ms
Pushing 4 at : 20s 1.052219ms
Pushing 5 at : 25s 0.926325ms
[ 'String 1', 'String 2', 'String 3', 'String 4', 'String 5' ]
Total Execution Time for For loop : 25s 3.62568ms
Execution Times for Delayed ForEach Loop ----
[]
Pushing 1 at : 5s 1.026379ms
Pushing 2 at : 5s 1.227689ms
Pushing 3 at : 5s 1.373413ms
Pushing 4 at : 5s 1.537165ms
Pushing 5 at : 5s 1.730363ms

Note that forEach carries out all the operations parallely at approximately 5 seconds and for loop does the some operations sequentially thus taking a total of about 25 seconds!

However, there is another alternative to block loops that also provides parallel execution and thus optimized performance

This is explained in the next post Click Here

1+

Leave a Reply