How Node.js Works

Wanuja Ranasinghe
Dev Genius
Published in
10 min readMar 19, 2024

--

Understanding Concurrency, Non-blocking I/O, and Event Loop in Node.js

Image by Author: How Node.js works
Image by Author: How Node.js works

In this article, we will delve into the internal workings of node.js. Whether you are a beginner or an experienced developer, understanding these concepts is essential. While many developers focus only on coding, it is crucial to understand how your code operates behind the scenes. Therefore, this article aims to emphasize key concepts that every developer should be familiar with. Moreover, if you are preparing for an interview, grasping these concepts could significantly benefit you. Let’s jump into the content.

✍️ Unwrapping the Content

  1. JavaScript on the Backend
  2. Node.js Architecture
    i) V8 JavaScript Engine
    ii) Libuv
  3. Single Thread Behavior in V8 Engine
    i) JIT Compilation in V8
    ii) Limitations of Single-Threaded Model for CPU-Bound Tasks
  4. Event Loop and Its Role in Managing Events and Callbacks
    i) Event Sources and the Event Queue
    ii) Prioritization and the Call Stack
    iii) Single-Threaded Execution
    iv) Non-Blocking I/O Operations
    v) Callback Queue and Asynchronous Flow
    vi) Processing Completed Tasks and the Loop Continues
  5. Role of Libuv in Facilitating Non-Blocking I/O
  6. Advantages and Disadvantages in Node.js
  7. Choosing the Right One
  8. Conclusion

Here are some jargons you need to know before dive in to the content.

  • Synchronous operations vs Asynchronous operations
    Synchronous
    execution involves tasks being executed sequentially, blocking the program until each task completes, whereas asynchronous execution allows tasks to run concurrently, enabling the program to continue executing other tasks while waiting for certain operations to finish in the background.
Asynchronous vs Synchronous Execution
Image from scaler.com: Asynchronous vs Synchronous Execution
  • Events and Callbacks
    Events
    in programming refer to occurrences or happenings within a system, such as a button click, file loading, or network request completion, while callbacks are functions that are passed as arguments to other functions and are invoked once a certain event or condition occurs, allowing for asynchronous execution and event-driven programming paradigms.
  • Single Thread vs Multi Thread
    In a single-threaded system, only one task is executed at a time, while in a multi-threaded system, multiple tasks can run concurrently, with each task having its own thread of execution, enabling parallel processing and potentially improving performance and responsiveness.
  • I/O bound vs CPU bound applications
    I/O bound applications primarily handle input/output operations like reading from files or making network requests, while CPU bound applications focus on intensive computational tasks that require significant processing power.

JavaScript on the Back-end

For years, JavaScript became supreme as the language of choice for web page interactivity. It made life into static HTML by adding dynamic elements, animations, and user-driven actions. But traditionally, JS resided only within the web browsers, limited to the client-side of web development.

Things have changed a lot. Node.js has made a big impact and changed the way things work. node.js is an open-source runtime environment that allows JavaScript code to execute outside the browser, providing the control of server-side scripting. This means JS can now handle the “backend” operations of a web application, the unseen engine that processes data, interacts with databases, and ultimately generates the response sent back to the user’s browser.

Node.js Architecture- Powering JavaScript on the Server

Node.js takes a unique approach to handle requests efficiently, using a combination of following powerful components:

1. V8 JavaScript Engine

The V8 engine is the heart of node.js, acting as the JavaScript execution engine. Originally developed by Google for Chrome, it’s known for its exceptional performance and ability to convert JavaScript code (usually written in a high-level format) into highly optimized machine code that the computer can execute directly. This optimized code runs exceptionally fast, making node.js applications responsive.

2. Libuv

While V8 handles JavaScript execution, node.js interacts with the operating system (OS) through a library called Libuv. Libuv is a cross-platform asynchronous I/O library written in C. It provides an event-driven mechanism for node.js to perform non-blocking I/O operations, allowing node.js to manage multiple concurrent requests efficiently by supporting to event loop and thread pool.

Single Thread Behavior in V8 Engine

As I mentioned earlier, Node.js is built on top of the V8 JavaScript engine, one of the fundamental characteristics of V8 is its single-threaded execution model. In a single-threaded environment, only one task can be executed at a time, which might raise concerns about performance and scalability, especially for CPU-bound tasks involving extensive calculations.

However, V8 excels at executing JavaScript code efficiently within this single-threaded context. It achieves this efficiency through various optimization techniques, including just-in-time (JIT) compilation.

  1. JIT Compilation in V8

V8 doesn’t interpret JavaScript code line by line in its raw form. Instead, it utilizes a technique called JIT compilation. During runtime, V8 identifies frequently executed code sections and translates them into highly optimized machine code specific to the underlying hardware architecture (CPU type, memory layout). This machine code executes significantly faster than the original JavaScript, leading to substantial performance gains.

2. Limitations of Single-Threaded Model for CPU-Bound Tasks

Despite V8’s optimization capabilities, the single-threaded nature of Node.js poses limitations for CPU-bound tasks that require extensive calculations. Since Node.js operates within a single thread, long-running CPU-bound tasks can block the event loop, leading to poor responsiveness and degraded performance for other tasks.

To mitigate this limitation, developers must adopt strategies such as offloading CPU-intensive tasks to worker threads or employing asynchronous processing techniques to avoid blocking the event loop.

Event Loop and Its Role in Managing Events and Callbacks

At the heart of Node.js’s concurrency model based on the event loop, a crucial component responsible for managing asynchronous operations and handling events and callbacks efficiently.

Imagine a conductor in an orchestra, ensuring each instrument plays its part at the right time. The event loop plays a similar role in Node.js, managing a continuous flow of events and ensuring tasks are processed efficiently. Here’s a breakdown of how it ensures smooth function execution while handling multiple requests:

High Level Architecture of Node.js Event Loop
Image from DigitalOcean — High Level Architecture of Node.js Event Loop

1. Event Sources and the Event Queue:

The event loop receiving requests from various sources. These requests, known as events, can be,

  • Client requests arriving from the network.
  • Timers expiring after a set delay.
  • I/O operations completing (e.g., database access, file reading/writing).

The event loop maintains a waiting line called the event queue. Events are added to this queue as they arrive.

2. Prioritization and the Call Stack:

  • Not all events have equal priority. The event loop may prioritize certain events, such as those demanding immediate attention (e.g., network errors).
  • Once an event is chosen, its corresponding JavaScript function, is pushed onto a separate stack called the call stack. This call stack holding the currently executing function.

3. Single-Threaded Execution:

  • As I mentioned, node.js utilizes a single-threaded V8 engine for JavaScript execution. This means only one function (from the call stack) can be executed at a time.

4. Non-Blocking I/O Operations:

  • Certain tasks, like database access, might involve waiting for external systems. If the event loop were to wait for these lengthy operations to complete, it would block the single thread and prevent other functions from execution.
  • Node.js addresses this through non-blocking I/O. When an I/O operation is initiated, the event loop doesn’t wait. Instead, it delegates the task to Libuv, a C library. Libuv can potentially utilize multiple threads in the thread pool to handle these operations efficiently, freeing up the single thread for JavaScript execution.

5. Callback Queue and Asynchronous Flow:

  • Once the I/O operation is complete (e.g., data retrieved from a database), Libuv doesn’t directly interrupt the ongoing JavaScript execution.
  • Instead, it sends a notification (callback) to the event loop, informing it that the task is finished. This notification includes any relevant data retrieved during the operation (e.g., retrieved database record).
  • These notifications are stored in a separate queue called the callback queue, functioning as a waiting area for completed tasks.

6. Processing Completed Tasks and the Loop Continues:

  • When the currently executing function finishes (on the call stack), and the event queue is empty (no more pending events), the event loop checks the callback queue.
  • If a notification is present (an I/O operation has finished), the event loop retrieves it and schedules the associated callback function for execution (added back to the call stack).
  • The callback function receives the data from the completed I/O operation and performs any necessary actions based on the retrieved data.

The event loop is a continuous process that listens for events and executes associated callbacks when triggered. It operates asynchronously, allowing Node.js to handle multiple tasks concurrently without blocking the main execution thread. This non-blocking approach ensures that Node.js remains responsive and can handle numerous concurrent operations seamlessly.

Role of Libuv in Facilitating Non-Blocking I/O

Libuv is a multi-platform library that provides asynchronous I/O support and abstracts operating system-specific details, allowing Node.js to perform non-blocking I/O operations efficiently. Under the hood, Libuv utilizes techniques like event notification mechanisms and thread pools to handle I/O tasks asynchronously, without directly introducing multithreading within the Node.js runtime.

By the way, what is this thread pool and where is it located?

  • The thread pool comes into play with Libuv. Libuv acts as a helper library, the threads used by Libuv’s potential thread pool are managed by the operating system itself. Node.js doesn’t directly control the creation or management of these threads. Libuv interacts with the operating system’s thread pool to offload I/O tasks, allowing them to run concurrently without blocking the single JavaScript thread.

By using Libuv’s capabilities, Node.js can effectively manage I/O-intensive operations while maintaining high concurrency and responsiveness, even under heavy loads.

Key Points Remember

  • Node.js uses a single-threaded V8 engine for JavaScript execution.
  • Libuv (a C library) can leverage a thread pool under the hood for efficient I/O operations.
  • The operating system manages the threads used by Libuv’s thread pool.

Advantages and Disadvantages in Node.js

Node.js offers a unique development paradigm, but it’s not a fits for all solution. Let’s explore its strengths and weaknesses to help you decide if it’s the right tool for your project.

  1. Advantages in Node.js
  • Concurrency and Scalability: Node.js excels at handling a high volume of concurrent requests efficiently. Its event loop architecture and non-blocking I/O enable it to scale effectively to meet increasing demands, making it ideal for real-time applications and I/O-bound APIs.
  • Performance: The V8 engine’s JIT compilation and single-threaded focus optimize JavaScript execution, leading to performant applications.
  • Rapid Development: Node.js ha a vast ecosystem of pre-built modules and frameworks, accelerating development by providing ready-made functionalities for common tasks. Additionally, using a single language streamlines the development process.

2. Disadvantages in Node.js

  • Limited CPU-Bound Performance: While Node.js excels at I/O-bound tasks, its single-threaded V8 engine can struggle with computationally intensive operations. For CPU-bound tasks, alternative approaches might be necessary.
  • Callback Hell: Asynchronous programming in Node.js often involves the extensive use of callbacks, which can result in deeply nested and complex code structures known as “callback hell.” Techniques like promises and async/await can help mitigate this issue.
  • Limited Standard Library: Node.js has a relatively small standard library compared to other platforms, which may require developers to rely on third-party libraries for certain functionalities, leading to dependency management challenges.

Choosing the Right One

Node.js offers a strong development environment, but like any tool, it has its ideal use cases and limitations. Here’s a breakdown to help you decide if Node.js is the perfect fit for your next project:

  1. When to use Node.js
  • Real-Time Applications: Node.js thrives in building applications that demand real time interactions. Its event driven architecture and non blocking I/O enable efficient handling of high volumes of concurrent connections, making it ideal for chat applications, collaboration tools, live stock tickers, social media feeds and live dashboards where constant data exchange is crucial.
  • I/O-Bound APIs: Node.js excels at creating APIs that heavily rely on I/O operations. Frequent database access, file processing, or API integrations with external services become a breeze with Node.js’s ability to handle them asynchronously without blocking the event loop. This translates to a more responsive and scalable API.
  • Microservices Architecture: The lightweight nature, scalability, and efficient handling of independent functionalities make Node.js a great choice for building microservices. Each microservice can be developed and deployed independently, promoting modularity and flexibility in your application architecture.
  • Full-Stack Development with JavaScript: If your project leverages JavaScript for both frontend and backend development, Node.js offers a unified approach.

2. When to consider Alternatives

  • CPU-Bound Applications: While Node.js excels at I/O-bound tasks, its single-threaded V8 engine can become a bottleneck for computationally intensive operations. If your application involves extensive calculations or complex scientific simulations, languages like C++, Java, or Python might be better suited due to their multithreaded capabilities.
  • Simple CRUD Applications: For straightforward applications that primarily involve basic Create, Read, Update, and Delete (CRUD) operations on a database, Node.js might be overkill. Simpler frameworks designed for specific backend languages like Python (Flask, Django) or PHP (Laravel) could be a more efficient choice for these scenarios.

📚Conclusion

In conclusion, Node.js achieves concurrency through a combination of event-driven architecture, non-blocking I/O operations, and efficient execution within a single-threaded environment. By utilizing the event loop and Libuv’s capabilities, Node.js can handle numerous concurrent tasks efficiently, making it a powerful platform for building scalable and high-performance applications. Understanding these core concepts is essential for developers to utilize the full potential of Node.js and build robust and responsive applications.

Thanks for reading ❤️.

--

--