Many performance-related issues in Node.js applications have to do with how promises are implemented. Yes, you read that right. How you implemented promise in your Node.js app is most likely the culprit for how slow your app has become 🙈.
Promise is one of the popular ways of implementing asynchronous programming in Node.js which is a really good improvement over callbacks. First I’ll like us to get the literal meaning of promise outside of Node.js:
a statement that tells someone that you will definitely do or not do something
The keyword in the definition above is “WILL” which signifies sometime in the future. It simply means that a promise is an assurance of something that will happen in the future.
This is the exact concept of promise in Node.js which denotes that when we try to perform some operations whose results we cannot get immediately, we instead get an “assurance” of the result that will be available sometime later. The important question then is “while we wait for the promise to get fulfilled, is it ideal to “idly” wait and not execute other parts of the program, especially the ones whose results can be readily available or not?”
The answer to the question above will inform how you would work with almost inevitable promises in various parts of your applications.
There are many ways of working with promises in Node.js but async/await is a really nifty approach that many have grown to love over the years since its introduction. The truth is lots of .then in promise chaining is not very easy to keep track of when they grow to a considerable length (see example here) neither is callback (Callback hell see here). So it’s understandable why many will choose the more convenient and cleaner async/await but which sometimes can be injurious to the overall performance of your application when not applied correctly.
async/await <--> async/ablock 🤐
The simple problem with it is that it is capable of slowing down your application greatly when not correctly used. Whenever a promise is marked with await in an async function, what you are saying is that, until the promise has resolved, the following code or code blocks in the function shouldn’t be executed which in itself is not a bad thing.
However, it becomes a problem when the code that follows can in fact be executed while waiting for the promise to get resolved because they are not dependent on the result of the resolved promise. Let’s consider the code below:
In the code snippet above, even though the two awaits were unrelated they still block each other. The second promise did have to wait for the first one to resolve before it starts which means it will take double the time for all the promises to get resolved.
Below is a better to handle the promises such that they don’t block each other while still using your lovely await 😉
Here we used await with promise.all to ensure that the two promises got executed in parallel which means instead of taking double the time as we had in the blocking example, the two promises got resolved together in ~2 seconds which was half the time of the blocking example. Now isn’t that good?
What to note here is that👇🏼
unrelated promises shouldn’t block each other
No! Depending on the case but most times, even dependent promises can be implemented in a way that ensures they are not blocking or the blocking gets reduced to the barest minimum for improved performance. Once again, let’s consider yet another example of this scenario:
Let’s say in an employee management system, you want to get the list of employees alongside their next of kin information.
In such a system, we first need to get the employee information and use that to find their next of kin which means we will have a dependent promise situation. Let us look at both the inefficient and a more efficient way to do this:
Below is the actual logic that determines how to work with the employee and next of kin data in the DB. This is where all the good and bad choices will matter:
Here, the second asynchronous operation had to wait for the first to complete before starting which is fine, but the problem is in using await inside the loop which every asynchronous operation (getting next of kin) had to wait for the one before it 😳 This is bad. Don’t do it.
One of the cases of bad use of async/await is inside a loop.
Majority of the time you can and should avoid it,
at least if the performance of your application and the
time of your user matters to you then you should.
Now let's look at the better approach below:
Notice that in the code snippet above since the second operation is dependent on the result of the first one and there are no other synchronous operations that will be blocked, as a result, we waited until all employee records are available before starting the next operation that gets their next of kin information.
However, instead of each iteration of promise to wait on the one before it, the promises were stored and executed in parallel which saves immense execution time than the first approach, and the entire operation finished in ~2 seconds as opposed to the first blocking example that took ~6 seconds to complete execution.
Another reason your Node.js application may be performing poorly is that you could be blocking the event loop in your code.
You can read more about the event loop here
Let's consider the example below:
Assuming that in your application you need to work with countries and you have a list of countries as an external CSV file which you need to access in your code. In the code snippet below, the file reading operation blocks the event loop and ultimately affects the application’s throughput and performance because until the file reading operation completes, nothing else gets executed.
Now, let’s consider a better way this can be done in a way that it doesn’t block.
Since the actual reading of the file is an I/O operation that does not require the event loop, this operation shouldn’t block and that is what is done here as the event loop is freed up to execute other parts of the application until the result of the file reading operation becomes available.
The code snippet above uses callback which is just another method of implementing asynchronous programming in Node.js. This can be easily converted to promise so that you can use your lovely async/await for the same. One way of doing that will be to wrap the file reading operation in a promise and make the returned value a promise.
There are definitely more reasons why your Node applications may perform poorly in terms of performance but these are the more common ones I have seen. You are welcome to share more insights in the comment section.
The key things to remember regardless of which approach you chose to use when working with promises in Node.js is to ensure:
- Unrelated promises do not block each other.
- Non dependent promises are executed in parallel and not sequentially.
- Don't use await inside a loop.
Regarding the event loop:
- Whatever you do, ensure that the event loop isn’t blocked.
If you can keep these in mind, you will be intentional about taking better decisions on which approach to use so that the performance of your application doesn’t suffer.
This article is majorly focused on a single approach to working with promises and its implications.
There are other ways/things to consider to achieve the same or sometimes better result when working with promises in Node.js which I encourage you to read up on in the links below:
Broken Promises - James Snell
Don't block the event loop - A Node.js guide on never blocking the event loop.
If you know of other ways of making asynchronous programming a bliss in Node.js, please do share in the comment section.