How does Node Js work behind the scene?

Nikhil Mahajan
5 min readDec 12, 2022

--

Laying The Foundation

Javascript is developed as a client site language to make web pages interactive. We as developers write js code but the machine does not understand such high-level language. This code needs to be converted into binary form so the machine could understand. Javascript Engine (V8, the most popular one) does this work of conversion. V8 Engine is mostly written in C++.

The Javascript Engine is obviously embedded in the browser. The browser comes with an environment that helps us to use many functions in js that are not fundamentally defined in js.

document.getElementById('')
// This is one such function

One of the awesome features of Javascript is that in spite of being a single-threaded language, it is still non-blocking. But how? Let's first understand what is meant by single-threaded. A thread is the smallest segment of instructions that can be handled independently by an operating system. When a program is under execution, then it is known as a process. For example, closing the chrome window. This process may give rise to multiple threads. In the case of Javascript process, it uses only a single thread. A single thread can only run on one core.

Now consider the following code where func2 takes infinite time for its execution. We will fire func1 through a button click on a simple HTML page.

func2(){
while(true);
}

function func1(){
console.log("started")
func2();
console.log("Ended")
}

All of the above code is going to execute on one core. As func2 takes infinite time for its execution then all other Javascript operations can not take place and the page become unresponsive (We will not even be able to select the text on the page). We have actually blocked the main thread.

The problem is solved using EventLoop. This EventLoop is responsible for the non-blocking nature of Js in the browser environment.

Development Of Node Js

Before 2009, the only way to execute Js code is to use the browser as it has an engine. A guy named Ryan Dahl embedded chrome’s V8 engine in the c++ environment. This environment like the browser provides a different set of functions like readFile or createServer. Therefore, Node Js is not a programming language but it is a Javascript runtime environment. It is built on top of V8 Engine and LibUv.

A Quick Look Over Source Code

Let’s try to find the readFileSync function (widely used function in Node Js) in the source code (/lib/fs.js).

function readFileSync(path, options) {
options = getOptions(options, { flag: 'r' });
const isUserFd = isFd(path); // File descriptor ownership
const fd = isUserFd ? path : fs.openSync(path, options.flag, 0o666);

const stats = tryStatSync(fd, isUserFd);

This function is written in js and calls many other js functions that are in the file itself. Going deeper into function calls, we find (process.binding).

 const result = binding.read(fd, buffer, offset, length, position,
undefined, ctx);

This read function is actually written in c++ and is exposed to the js world through the binding process. Now, let’s search for the read function (/src/node_file). This contains the c++ side of Node Js.

static void Read(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
const int argc = args.Length();
CHECK_GE(argc, 5);
CHECK(args[0]->IsInt32());

There is if else statement at the end part of this Read function. This is actually checking whether it is asynchronous ( if it is then there will be call back function passed as a parameter) or synchronous (passed as undefined as above).

  if (req_wrap_async != nullptr) {  
// read(fd, buffer, offset, len, pos, req)
FS_ASYNC_TRACE_BEGIN0(UV_FS_READ, req_wrap_async)
AsyncCall(env, req_wrap_async, args, "read", UTF8, AfterInteger,uv_fs_read, fd, &uvbuf, 1, pos);
// asynchronous
} else {
// read(fd, buffer, offset, len, pos, undefined, ctx)
CHECK_EQ(argc, 7);
FSReqWrapSync req_wrap_sync;
FS_SYNC_TRACE_BEGIN(read);
// synchronous
}

This part is where c++ functions are exposed to Js.

  registry->Register(Read);
registry->Register(ReadBuffers);
registry->Register(Fdatasync);

Here, the file imports V8’s inbuilt functions and classes. Therefore, readFileSync is a Js function that calls another Js functions that at the end calls the c++ function and this c++ function uses V8 properties to provide us with useful modules.

Diving deep into Node Js

As Node Js is built using Js so obviously it is also fundamentally single-threaded. We use Node Js on the server side and the server needs to handle multiple requests at a time. Node Js have a component called LibUv that is responsible for the non-blocking and asynchronous behavior.

The functions that require to talk to other servers or doing operations like file reading are not directly executed in the main thread as they block the main thread. The moment Node Js process begins event loop started. It continuously looks at the main thread for offloading such readFile or I/O based functions into the thread pool. This thread pool gives Node Js a sort of multi-threaded behavior as well.

When these offloaded functions complete their execution in the thread pool they emit events. EventLoop listens to these events and store callback functions associated with these events in callback queues. The callback functions wait in the queue for their execution in the main thread. As the main thread becomes empty event loop pushes a function from the queue to the main thread. Therefore, Node Js has the Event-Driven Architecture and the event loop is said to be the heart of this architecture.

The EventLoop loops through various phases and each phase has its own callback queue. In the first phase, it checks whether there are expired timer callbacks in a queue or not. If callbacks are there, then it pushes all these callbacks to the main thread one by one. Afterward, it heads to the next phase and repeats the same steps.

Thread Pool's default size is 4, but it can be changed at startup time by setting the UV_THREADPOOL_SIZE environment variable to any value (the absolute maximum is 1024). Obviously, setting thread pool size more than the number of cores in the machine makes no sense.

This article may contain mistakes and I would be grateful to you if you report it to me.

Thanks for reading…
Happy coding !!

--

--