Node.js is an open source cross-platform JavaScript runtime environment for executing JavaScript outside of the browser. It’s powered by Google’s V8 engine, which makes it very good.

Asynchronous event-driven runtime

One of the most common statements we encounter when introducing Node is that it runs on a single thread. That said, everyone is probably wondering how Node could possibly be one of the most popular tools for building fast and extensible apis?

Technically, the fact that Node.js uses a single thread is not 100% correct. Node actually uses many threads, but the Event loop event loop (which we’ll cover later) and user code run on the same thread. If we read the documentation carefully, we’ll see that Node uses an event-driven, non-blocking I/O model, which makes it lightweight and efficient.

What is the event-driven non-blocking I/O model?

As you know in the Node guide, blocking methods are executed synchronously and non-blocking methods asynchronously. Suppose we had to write some code to read the contents of a file and print them out in the console. There are two ways to do this in Node: synchronous and asynchronous. Let’s look at the sync version first:

The code above does the following: First, it needs the FS Module. In the second line, the readFileSync method is called and the result is stored in the data variable. The main thread of Node blocks on this line until all the contents of the file have been read. The contents are then recorded on the console, and a “done” statement is printed. Now let’s look at the same code that executes asynchronously:

In this example, the readFile method is used and it executes asynchronously. Once this line is encountered, control is passed to Libuv, where the file is read. This is not done on the main thread. Instead, use worker threads from the Libuv thread pool (the default is four threads). After reading, the corresponding callback is pushed to the queue used by the event loop. In the next iteration of the event loop, during the callback execution phase, the callback is pushed to V8’s call stack and finally executed. All of this is done in the background, and Node’s main thread is only responsible for performing callbacks. So, going back to the example above, the “Done” statement will print first and then log the result of reading the file. That’s what “non-blocking I/O” means, and that’s why in every Node.js guide you read, people recommend using asynchronous methods instead of their synchronous versions.

How does Node behave differently from other Web servers?

Node’s event-driven runtime behaves quite differently than a multi-threaded server. In a multi-threaded server, each connection creates a new thread to process the request, and all work done in that thread can be blocked without affecting other connections (that is, you can query the database, wait for the results, and then do some other work). Since there are now many cores per CPU, this approach takes good advantage of processor power. However, there are many challenges. In a multi-threaded environment, each thread adds some overhead because it requires memory, which means a limited number of threads can be used. What happens if this limit is reached? The new connection will eventually time out. In addition, if the application is primarily I/O constrained, each thread will waste a significant amount of time waiting for results from the network or disk. Node, on the other hand, handles everything in a single thread. Similar to the file operations we explained above, event loops act as schedulers, constantly listening for new events and delegating work to the kernel or other worker threads. It never blocks (unless told to do so). Therefore, the server can accept new client connections, do some other work, and then continue accepting new client connections again. The client connection does not need to allocate a new thread, it just needs a socket handler managed by the kernel. This approach is fast, lightweight, and scalable, which is the main reason Node can handle high concurrency.

The Node’s runtime architecture

The Node runtime is designed to be multi-layered, with user code at the top and each layer using the API provided by the layer below

Node.js runtime architecture

  1. User code: Javascript application code written by programmers.
  2. Node. Js API:Node provides built-in methods that can be used in user code (for example, for using HTTP methods)HTTPModules,cryptoModule, for file system operationsfsModule, for network requestsnet, etc…) . For a complete list of the methods Node provides, you can find them in thehereReview the documentation. In addition, you can use thehereFind the source code implementation. Node’s API is written in Javascript.
  3. Bindings and C++ extensions:As you read Node, you’ll see that V8 is written in C++, Libuv is written in C, and so on. Basically, all modules are written in C or C++ because those languages are so good and fast at handling low-level tasks and using OS apis. But how can upper-level Javascript code be written in another language? This is thebindingsThe role of. They act as the glue between the two layers, so Node can easily use low-level code written in C or C++. So what if we want to add a C++ module ourselves? We first implement the module in C++ and then write for itbindingsThe code. This piece of code we wrote is called an extension. More information can be found herehereTo find it.
  4. Node’s dependencies: This layer represents the underlying libraries used by Node. The biggest reliance is Google’s V8 engine and Libuv. Other libraries include OpenSSL (for SSL, TLS, and other basic encryption features), HTTP parser (for parsing HTTP requests and responses), C-ARES (for asynchronous DNS requests), and Zlib (for fast compression and decompression).
  5. Operating system: This is the lowest level of OS APIS (system calls) used to represent the above libraries. Since OS-ES is different, these libraries include implementations of Windows and Unix variants, making Node platform independent.

About V8 and Libuv

Libuv is a library written in C for abstracting non-blocking I/O operations. It provides the following functions:

  • Event loop
  • Asynchronous TCP and UDP Sockets
  • Asynchronous DNS resolution
  • Asynchronous file and file system operations
  • The thread pool
  • The child process
  • High-resolution clock
  • Threading and synchronization primitives
  • polling
  • Streaming
  • Pipes

V8 is the library that provides the Javascript engine for Node.js. It is a JUST-in-time (JIT) compiler, which means it switches continuously between compiling and running blocks of code. The advantage of compiling and running alternately is that it can gather information while running the code and infer what will happen in the future based on the data received. These projections are useful for compiling better code.