Let's Dive into Internal Working of Node.js

Let's Dive into Internal Working of Node.js

Have you ever wondered how Node.js can handle thousands of connections at once without breaking a sweat? As a popular choice for building scalable network applications, Node.js has revolutionized the way developers approach server-side programming. At its core, Node.js is a JavaScript runtime built on Google’s V8 engine, designed to make asynchronous programming seamless and efficient.

In this blog, we’ll take a closer look at the inner workings of Node.js, exploring its event-driven architecture, the powerful V8 engine, and how these components come together to create a robust platform for modern web applications. Whether you’re a seasoned developer or just starting, understanding these fundamentals will enhance your ability to harness the power of Node.js in your projects.

Event Driven Architecture

At the heart of Node.js lies its event-driven architecture, a design paradigm that allows it to handle multiple operations simultaneously without blocking the execution thread. This model is particularly well-suited for I/O-heavy applications, such as web servers, where waiting for network responses or file reads could slow down overall performance.

How It Works

  1. Event Loop: The core of Node.js’s event-driven model is the event loop. When a Node.js application starts, it initializes the event loop, which continuously checks for events (like incoming requests) and executes the associated callbacks. This allows Node.js to remain responsive, as it can process other operations while waiting for events to complete.

  2. Non-Blocking I/O: Unlike traditional server models that rely on a multi-threaded approach (which can consume a lot of memory), Node.js uses non-blocking I/O operations. When a task (like reading a file or making a database query) is initiated, Node.js sends the request and moves on to the next task without waiting for the previous one to finish. Once the operation completes, the callback function associated with that task is called, allowing the application to handle the result.

  3. Callbacks and Promises: In Node.js, callbacks are a fundamental way to handle asynchronous operations. However, to improve readability and manageability, developers often use promises and the async/await syntax, which help avoid “callback hell” and make the code easier to follow.

Benefits

  • Scalability: The non-blocking nature of Node.js allows it to handle thousands of concurrent connections efficiently. This makes it an ideal choice for applications that require real-time capabilities, such as chat applications or online gaming.

  • Performance: By leveraging asynchronous programming, Node.js can perform tasks faster than traditional, synchronous models. This responsiveness enhances user experience, especially in applications where speed is critical.

What is V8 Engine?

V8 is the name of the JavaScript engine that powers Google Chrome. It's the thing that takes our JavaScript and executes it while browsing with Chrome.V8 is the JavaScript engine i.e. it parses and executes JavaScript code. The DOM, and the other Web Platform APIs (they all makeup runtime environment) are provided by the browser.

JavaScript is generally considered an interpreted language, but modern JavaScript engines no longer just interpret JavaScript, they compile it.

This has been happening since 2009, when the SpiderMonkey JavaScript compiler was added to Firefox 3.5, and everyone followed this idea.

JavaScript is internally compiled by V8 with just-in-time (JIT) compilation to speed up the execution.

How these all Align perfectly?

So before moving further, I just want to tell you that whenever we talk about Node.js and its performance, two images that you should think about in your mind are the V8 Engine (which compiles and executes the JavaScript code) that we covered, and the second one is Libuv (a C++ Library) which handles the thread pool and event loop. Now let's get a little deeper into it.

The Main Thread and Execution Flow

There is a main thread which executes all of your code in the given fashion. So first of all, it processes the Top-level Code, then any require Module statements, then any event callback registrations, and then the event loop starts and continues until every task gets completed.

The Role of the Thread Pool

You might ask, "What's the use of the Thread Pool?" Well, whenever we encounter a CPU-intensive task, it might block the Main thread. At that time, we use our Thread Pool and delegate these tasks to be executed there, returning the result back to the main thread when complete. At most, we can use 128 threads in the pool.

Event Loop in Detail

The event loop, is the heart of Node.js's non-blocking I/O model. It consists of several phases:

  1. Expired Timer Callbacks: Executes callbacks for timers that have elapsed.

  2. I/O Polling (FS): Checks for and handles any pending I/O events, particularly file system operations.

  3. setImmediate Callbacks: Runs any callbacks scheduled using setImmediate().

  4. Close Callbacks: Executes 'close' event callbacks.

  5. Pending Check: Determines if there are any pending callbacks to process.

If there are pending callbacks, the loop continues; otherwise, the process exits.

Node.js Process Architecture

The Node.js process consists of two main components:

  1. Main Thread:

    • Init Project: Sets up the Node.js environment.

    • Top Level Code: Executes the main script.

    • Require Module: Loads and processes required modules.

    • Event Callback Register: Sets up event listeners and callbacks.

    • Event Loop Start: Initiates the event loop.

  2. Thread Pool:

    • Contains between 4 and 128 threads.

    • Handles CPU-intensive tasks and certain types of I/O operations.

How Node.js Achieves Non-Blocking I/O

Node.js achieves non-blocking I/O through its event-driven architecture:

  1. When an I/O operation is initiated, Node.js doesn't wait for it to complete.

  2. Instead, it registers a callback and continues executing other code.

  3. When the I/O operation completes, the callback is placed in the appropriate queue.

  4. The event loop processes these callbacks in the order determined by its phases.

This approach allows Node.js to handle many concurrent operations efficiently, making it ideal for applications with high I/O requirements.

Simple Code to Digest the Concept.

const fs = require('fs');
const crypto = require('crypto');

console.log('Program start');

// 1. Top-level code (runs immediately)
const startTime = Date.now();

// 2. Registering an immediate callback
setImmediate(() => {
  console.log('This runs in the next iteration of the event loop');
});

// 3. Setting up a timer
setTimeout(() => {
  console.log('Timer callback after 0ms (runs after immediate and I/O callbacks)');
}, 0);

// 4. Asynchronous file I/O operation
fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('File read error:', err);
    return;
  }
  console.log('File contents:', data);
});

// 5. CPU-intensive task using the thread pool
crypto.pbkdf2('password', 'salt', 100000, 512, 'sha512', (err, derivedKey) => {
  if (err) throw err;
  console.log('Password hashing complete');
});

// 6. Another timer to demonstrate order of execution
setTimeout(() => {
  console.log('Another timer callback');

  // 7. Nested setImmediate and process.nextTick
  setImmediate(() => {
    console.log('Nested setImmediate');
  });

  process.nextTick(() => {
    console.log('Nested process.nextTick');
  });

}, 0);

// 8. process.nextTick callback
process.nextTick(() => {
  console.log('This runs before the next event loop iteration');
});

console.log('Program "end"');

// 9. Event loop keeps running due to pending async operations

Here's what's happening in this code:

  1. The top-level code runs immediately when the script is executed.

  2. setImmediate schedules a callback to run in the next iteration of the event loop, after I/O events.

  3. The first setTimeout schedules a callback to run after the timer expires. Even with a 0ms delay, it runs after immediate and I/O callbacks.

  4. fs.readFile is an asynchronous I/O operation. Node.js doesn't block here; it registers the callback and continues execution.

  5. crypto.pbkdf2 is a CPU-intensive task that uses the thread pool. This prevents blocking the main thread.

  6. Another setTimeout is set up to demonstrate the order of execution.

  7. Inside this timer callback, we nest a setImmediate and process.nextTick to show how they behave when called from within a callback.

  8. process.nextTick schedules a callback to run at the end of the current operation, before the next event loop iteration.

  9. The event loop continues running because there are pending asynchronous operations.

When you run this script, you'll see that the output doesn't appear in the order the code is written. Instead, it follows the Node.js event loop phases:

  1. Top-level code runs first.

  2. process.nextTick callbacks run.

  3. Timer callbacks execute.

  4. I/O callbacks (like file reading) run as they complete.

  5. setImmediate callbacks execute.

  6. The CPU-intensive task (password hashing) likely finishes last.

This demonstration showcases how Node.js can perform multiple operations concurrently without blocking, leveraging its event-driven, non-blocking I/O model.

Conclusion

I hope you found this exploration of Node.js internals insightful! If you enjoyed it, please consider sharing it with your friends and colleagues.I’m committed to bringing you more in-depth topics in the future. To stay updated on new content, don’t forget to subscribe to our free newsletter!

Thank you for joining me, and happy coding!👨‍💻👨‍💻

Did you find this article valuable?

Support Vishal Sharma by becoming a sponsor. Any amount is appreciated!