Under The Hood Of Node JS

Node is a Javascript server-side runtime; it’s not a framework, a library or a language. Although Javascript’s Google V8 engine works as a single-threaded compiler, Node is asynchronous in nature and built on callback functions and promises as a non-blocking I/O built on an event loop called lubUv.

In some ways, Node works similar to how web APIs from browsers work on the client-side of Javascript. The Javascript is compiled on the backend by the Google V8 engine through a call stack and the heap holds variables in memory. However, instead of processing asynchronous functions through web APIs from browsers, Node as a server-side backend utilizes core modules and external APIs that act as C++ bindings.

With syntax closer to machine code, C++ works faster than Javascript. Node JS changed the game because it allowed developers to write Javascript code with the performance of a C++ server-side backend. Node JS itself is written in C++ and it runs as efficiently as C++ or C languages. In essence, Node JS is a set of C++ libraries on top of the Google V8 Javascript compiling engine that allows Javascript to work on the server-side backend.

For example, file system (fs) is a C++ binding. Here is example code from a Node file written in Javascript that directly references a C++ binding.

const binding = internalBinding('fs');

const { FsReqCallback, statValue } = binding;

The internalBinding loads NodeFile.cc which is a C++ file that loads the fs namespace and from this fs namespace it reads the FsReqCallback. C++ bindings can not be processed in the Javascript call stack. They must get processed in an external API based in C++ bindings. Since C++ is a multi-threaded language, Node uses hidden classes to allow Javascript and C++ to work together and this will be discussed in detail in a future blog post.

The event loop in Node called libUv is provided by Node itself. Prior to ES6, there was no consensus rules on how an event loop should prioritize macrotasks (including the callback queue), microtasks (job tasks) and the call stack. Fortunately after ES6, modern event loops have uniform functionality. LibUv is an open-source C-dependent code written exclusively for Node; it’s not part of the Google V8 engine nor a part of Javascript. LibUv does more than just the event loop– it also processes TCP & UDP, DNS resolution, fs events like ReadFile, IPC and child processes.

The libUv event loop has two types of tasks: microtasks (job tasks like promises) and macrotasks (the callback queue or event queue). The microtask queue in the event loop handles promises and will always be prioritized before the macrotask queue. Every loop in the event loop is one tick. The job tasks like promises are executed first. Because promises can reference promises that can reference promises, there is a variable called processMaxTick to limit job tasks to 1000 jobs. The job tasks always run first in the first tick in the event loop up to the processMaxTick limit, and then on the next tick the macrotasks including the callback queue tasks are run in the event loop.

Whenever a C++ type of asynchronous function is called in Node, it will follow instructions to send it to an external API that processes C++ bindings. Unlike the call┬ástack, the macrotask queue (i.e. callback queue) follows the FIFO order (First In, First Out), meaning that the calls are processed in the same order they’ve been added to the queue. As mentioned above, the event loop handIes macrotask queues and microtask queues (job tasks). Eventually, the invoked callback task reaches the front of the callback macrotask queue and is picked up by the event loop and is executed in the call stack. Per ES6, the event loop constantly checks to see if the call stack is empty. Empty as in all of the synchronized code functions have been popped out (removed) from the call stack. Whenever the call stack is empty, the event loop will check the macrotask callback queue for any waiting messages, starting from the oldest message. Once it finds one, it will add it to the stack, which will execute the function in the message.

For me, it’s very helpful to understand what’s going on under the hood with Node as a server-side runtime process.