Node.js Architecture - The Event Loop

Node.js Architecture - The Event Loop

Daily short news for you
  • For a long time, I have been thinking about how to increase brand presence, as well as users for the blog. After much contemplation, it seems the only way is to share on social media or hope they seek it out, until...

    Wearing this shirt means no more worries about traffic jams, the more crowded it gets, the more fun it is because hundreds of eyes are watching 🤓

    (It really works, you know 🤭)

    » Read more
  • A cycle of developing many projects is quite interesting. Summarized in 3 steps: See something complex -> Simplify it -> Add features until it becomes complex again... -> Back to a new loop.

    Why is that? Let me give you 2 examples to illustrate.

    Markdown was created with the aim of producing a plain text format that is "easy to write, easy to read, and easy to convert into something like HTML." At that time, no one had the patience to sit and write while also adding formatting for how the text displayed on the web. Yet now, people are "stuffing" or creating variations based on markdown to add so many new formats that… they can’t even remember all the syntax.

    React is also an example. Since the time of PHP, there has been a desire to create something that clearly separates the user interface from the core logic processing of applications into two distinct parts for better readability and writing. The result is that UI/UX libraries have developed very robustly, providing excellent user interaction, while the application logic resides on a separate server. The duo of Front-end and Back-end emerged from this, with the indispensable REST API waiter. Yet now, React doesn’t look much different from PHP, leading to Vue, Svelte... all converging back to a single point.

    However, the loop is not bad; on the contrary, this loop is more about evolution than "regression." Sometimes, it creates something good from something old, and people rely on that goodness to continue the loop. In other words, it’s about distilling the essence little by little 😁

    » Read more
  • Alongside the official projects, I occasionally see "side" projects aimed at optimizing or improving the language in some aspects. For example, nature-lang/nature is a project focused on enhancing Go, introducing some changes to make using Go more user-friendly.

    Looking back, it resembles JavaScript quite a bit 😆

    » Read more

Node.js utilizes the Event Loop to handle asynchronous I/O tasks. Do you truly understand how the Event Loop operates?

Problem

In the previous article, we learned that the Event Loop is a critical component in JavaScript/Node.js. Its job is to return the callback functions from the Callback queue back to the call stack. But the story doesn't end there; the Event Loop has different ways of returning these functions. In other words, callback functions have different priority levels. The higher the priority, the faster the Event Loop returns it to the call stack. Understanding this mechanism will help readers write performance-optimized programs.

Let's first explore the phases of the Event Loop to understand the priority order of callback functions.

Phases of the Event Loop

Imagine the Event Loop as a wheel divided into multiple segments, specifically six in this case. To complete one cycle, it must pass through all six segments. At each segment, the Event Loop checks if any callback functions meet the criteria to be "picked up" and returned to the call stack.

Below is a diagram of the phases of the Event Loop.

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

Now let's dive into the details of each phase.

timers

Timers handle the callbacks of setTimeout and setInterval. When the specified waiting time has elapsed, the corresponding callback will be executed here.

It should also be noted that this is the minimum waiting time, not an absolute value, as it depends on the delays caused by other phases.

Example:

const fs = require('node:fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();
  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

Assume someAsyncOperation takes 95ms to complete, which is shorter than the 100ms wait time of the setTimeout function. In theory, the console.log statement in setTimeout should print a delay of exactly 100ms, matching the wait time. However, note that the callback of someAsyncOperation is "stuck" in the poll phase (where the while loop occupies the call stack), causing a slight delay before the callback in setTimeout can be added to the call stack. At this point, we observe a delay slightly greater than 100ms.

pending callbacks

Pending callbacks handle the callback functions for certain system-level operations, such as TCP errors or file system I/O errors (like fs).

idle, prepare

Idle, prepare is a phase primarily used internally by Node.js rather than user code.

poll

Poll is the central phase of asynchronous processing. The poll phase waits for and processes most I/O events, such as file read/write operations, network connections, etc. The poll phase has two main functions: calculating the wait time and processing events in the poll queue.

When the Event Loop enters the poll phase, it performs two tasks:

  • If there are I/O callbacks: execute them immediately.
  • If there are no I/O callbacks and:
    • There are setImmediate callbacks: exit the poll and move to the check phase.
    • There are no setImmediate callbacks and the wait time is not over: continue waiting.

The poll phase has a certain wait time threshold when it is idle before transitioning to the next phase.

check

Check handles the callback functions registered via setImmediate.

close callbacks

Close callbacks handle the callback functions when a connection resource is abruptly closed. For example, socket.destroy() or the close event being emitted.

Apart from the six phases above, Node.js has a "special phase" called process.nextTick().

process.nextTick()

process.nextTick() does not appear in the diagram of the six phases of the Event Loop, even though it is part of the asynchronous API. This is because, technically, it is not part of the Event Loop. Instead, the callback function of process.nextTick() is processed immediately after the current phase completes. In other words, it is always given the highest priority whenever the Event Loop moves to the next phase.

Because process.nextTick() is always executed before moving to the next phase, it has the ability to block the Event Loop, as demonstrated in the example below.

function endlessLoop() {
  process.nextTick(endlessLoop);
}
endlessLoop();

In the next article, we will dive deeper into understanding process.nextTick().

Conclusion

The Event Loop is a core component in the architecture of Node.js, orchestrating asynchronous tasks by bringing callback functions from the Callback queue into the Call Stack for execution. In this article, we explored the six main phases of the Event Loop, including timers, pending callbacks, idle, prepare, poll, check, and close callbacks, with each phase responsible for handling different types of callbacks. Timers are responsible for setTimeout and setInterval functions, while Poll is the central phase for processing I/O events and deciding transitions between phases. Additionally, setImmediate in the Check phase and abrupt callbacks in Close each play distinct roles in the mechanism. Notably, process.nextTick(), although not part of the Event Loop, has the highest priority, ensuring its callback is executed immediately after the current phase.

Understanding how the Event Loop operates and the priority order of each phase is key to optimizing the performance of Node.js programs. This knowledge helps you better control asynchronous tasks, minimize delays, and enhance overall efficiency. In the next article, we will explore process.nextTick() in-depth to fully harness the potential of Node.js.

Reference:

Premium
Hello

5 profound lessons

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!

View all

Subscribe to receive new article notifications

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.

Comments (0)

Leave a comment...