Anyone who delves deeper into Node.js may have seen articles distinguishing between setTimeout
, setImmediate
, and process.nextTick
. I am no exception! At first, I tried to understand how to use them by reading articles and even Node documentation. However, they were mostly theoretical. It's understandable because to truly understand their differences and usage, you need to understand the event loop.
In reality, I rarely care about the differences between them. That is, the program still runs even if you don't know or understand them, because you may not need to use them. However, if you want your program to run even better, you definitely need to learn how to use them.
Before we dive into this article, I recommend that you spend some time reading up on the concept of the event loop and how it works. Some of my previous articles on this topic are: Kiến trúc Node.js - Event Loop, Tìm hiểu về vòng lặp sự kiện (Event Loop) trong Node.js. Even better, you should read the valuable documentation from Node: The Node.js Event Loop. However, if you want to explore them from my perspective, please continue reading this article!
The event loop is the heart of Node.js, an infinite loop that pushes asynchronous function callbacks back to the call stack and handles them.
Let's recall the six phases of the event loop. One cycle of the event loop must go through the six phases before starting a new one. The six phases are as follows:
Timers phase: Handles callbacks scheduled using setTimeout
and setInterval
.
Pending callbacks phase: Handles callbacks delayed from previous I/O phases.
Idle, prepare phase: Reserved for Node.js internal operations.
Poll phase: Handles new I/O tasks and checks for completed events. If no events are present, it waits for new events.
Check phase: Handles callbacks from setImmediate
.
Close callbacks phase: Handles close events of resources (such as socket.on('close', ...)
).
Each phase handles separate events. Node.js divides the phases because of its single-threaded nature. We cannot push events into the loop and expect them to be processed in order, as some events need to be handled before others. Dividing the phases ensures this!
In one loop cycle, Node.js goes through each phase and handles events in it before moving to the next phase. Here, we see that setTimeout
is handled in the Timers phase, the first phase of the event loop. Wait! setImmediate
is handled in the Check phase, after the Timers phase. Does this mean setTimeout
with a timeout of 0 is always executed before setImmediate
? Where does the "immediate" name come from?
You've started to step into Node's complexity. The answer to the question is not always. It depends on the usage scenario, and that's the answer.
In the case where no I/O tasks are being handled, i.e., a completely synchronous program. When using setTimeout(fn, 0)
, the callback function is scheduled in the "timers" phase. If using setImmediate
, the callback function will be executed in the "check" phase. According to theory, the setTimeout
callback is executed before setImmediate
.
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
However, in reality, the results of setTimeout
and setImmediate
will be randomly executed, depending on which phase Node is currently in. This is because when a program first starts, Node has some initial latency before entering a stable state, meaning it's in either the "timers" or "check" phase.
In the case where I/O tasks are present, the order of execution will be different. Always, setImmediate
will be executed first because it is executed immediately after the I/O task is completed, as it lies in the "check" phase of the event loop, which occurs right after the "poll" phase (the phase handling I/O tasks).
setTimeout(fn, 0)
will only be executed after the event loop has returned to the "timers" phase, meaning it will have to wait for another event loop cycle to reach the "timers" phase.
fs.readFile("README.md", () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
Thus, we should use setImmediate
to run a callback function immediately after an I/O task is completed.
setImmediate
proves to be useful when used to log requests to a file. Let's look at an example of an HTTP server:
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World\n");
// Log to file (less important task)
setImmediate(() => {
fs.appendFile("server.log", `Request: ${req.url}\n`, (err) => {
if (err) throw err;
console.log("Logged request to server.log");
});
});
Many people think that using setImmediate
is unnecessary here because fs.appendFile
is an asynchronous function and does not affect the current session's response speed. Therefore, one could remove setImmediate
to avoid redundant code.
That perspective is correct if only you are using it. In reality, the server receives many requests simultaneously. Using setImmediate
pushes the log task to the Check phase, leaving time for the Poll phase – which is handling the primary function callbacks. Doing this significantly improves the program's performance, moving less important tasks to the Check phase, which is after the Poll phase.
setImmediate
is also used to reduce the main thread load. In my previous article Hai kỹ thuật nhằm ngăn vòng lặp sự kiện (Event Loop) bị chặn khi xử lý tác vụ nặng (CPU-intensive task), I discussed the "Partitioning" method based on setImmediate
to prevent blocking the event loop when handling a large amount of data.
Let's recall the six phases again. You won't see any phase handling process.nextTick
callbacks. Why is that? It's because process.nextTick
is a special function whose callbacks are always handled at the beginning of each event loop phase.
Let's take an example. The event loop handles the "timers" phase -> handles process.nextTick
callbacks -> handles the "pending callbacks" phase -> handles process.nextTick
callbacks… and so on. That means process.nextTick
has the highest priority in the event loop.
So, how do we apply process.nextTick
? The answer is clear: whenever we want to perform a task with the highest priority, before any phase is handled, to ensure the task is executed immediately. For example, process.nextTick
is also often used in the "Partitioning" method, just like setImmediate
, but with one difference: while setImmediate
is handled only once per loop, process.nextTick
is handled six times.
Therefore, in the bcrypt library, which is used for hash calculation – a very CPU-intensive task that can block the event loop – process.nextTick
is applied extensively to prevent this behavior.
It is also often used to reduce the main thread load during the initial startup but still wants the configuration loaded right after:
class MyClass {
constructor() {
process.nextTick(() => {
this.loadConfiguration();
});
}
loadConfiguration() {
// load config here!
}
}
const myClass = new MyClass();
Hello, my name is Hoai - a developer who tells stories through writing ✍️ and creating products 🚀. With many years of programming experience, I have contributed to various products that bring value to users at my workplace as well as to myself. My hobbies include reading, writing, and researching... I created this blog with the mission of delivering quality articles to the readers of 2coffee.dev.Follow me through these channels LinkedIn, Facebook, Instagram, Telegram.
Comments (1)