Grokking Node.js
Vote

0% completed

​

Understanding the Node.js Runtime Environment

What Changes from the Browser

The Parts of the Runtime

The Thread Pool Has Four Threads

Built-in Modules

npm Comes with Node.js

Why the Runtime Matters When You Write Code

Run these five lines in Node.js and in a browser console, and the answers differ:

console.log(typeof window); // the browser's global object console.log(typeof document); // the web page console.log(typeof process); // the running Node.js program console.log(typeof require); // the CommonJS module loader console.log(globalThis === global);

Node.js prints:

undefined undefined object function true

The language is the same in both places, but the objects around it are not. Everything that your code can use besides the language itself is provided by the runtime environment. This lesson explains what the Node.js runtime environment contains and how its parts work together when your code reads a file.

What Changes from the Browser

A browser builds its environment around a web page. It provides window for the browser tab, document for the page content, and functions such as fetch and setTimeout. It also keeps the page away from your files, because the page comes from a website that you may not trust.

Node.js builds its environment around a program running on your computer. It has no page, so window and document do not exist. Instead it provides process, which describes the running program, and require, which loads modules. The global object is called global, and the standard name globalThis points to it, just as globalThis points to window in a browser. Some functions, such as setTimeout and console.log, exist in both environments, and fetch is also global in Node.js 18 and later.

The larger difference is access. A Node.js program can read and write any file that your user account can, open network ports, and start other programs. That access is the reason Node.js can be a web server, and it is also the reason to run only code that you trust.

The Parts of the Runtime

A runtime is made of several parts that each have one job. They are easiest to understand by following one operation through them. The running example is a script that reads a file:

const fs = require('fs'); fs.readFile('notes.txt', 'utf8', (err, text) => { console.log('File contents:', text); }); console.log('Reading started');

When notes.txt contains the line Buy milk, the script prints the last line of its code first:

Reading started File contents: Buy milk

Five parts take part in running it, in this order.

1. V8 runs your JavaScript. V8 is the JavaScript engine from Google's Chrome browser. It first translates your code into a compact form called bytecode and runs that bytecode with an interpreter named Ignition. When a function runs often, V8 compiles it into machine code for your processor with its just-in-time (JIT) compilers, the fastest of which is named TurboFan. V8 knows the JavaScript language but nothing about files or networks.

2. A built-in module receives the request. The call to fs.readFile goes to the fs module, one of the modules that come with Node.js. Its JavaScript code passes the request to C++ code inside Node.js, which is the part that is able to talk to the operating system.

3. libuv performs the operation. libuv is a C library inside Node.js that handles all I/O and contains the event loop. For network connections, libuv asks the operating system to report when data arrives, and nothing waits on a thread. Many operating systems have no such non-blocking method for files, so libuv reads files on a small group of background threads called the thread pool. Meanwhile fs.readFile has already returned, and V8 runs the next line, which prints Reading started.

4. The event loop queues the callback. When the read finishes, libuv records the event. The event loop sees it the next time it checks for finished work.

5. V8 runs the callback. The event loop calls your callback on the main thread, and V8 runs it with the file contents in text.

One fs.readFile call travels from V8 through the fs module and libuv to the operating system, and its callback returns through the event loop.
One fs.readFile call travels from V8 through the fs module and libuv to the operating system, and its callback returns through the event loop.

Follow the numbered arrows in the diagram. Your JavaScript only appears at the top, in steps 1 and 5. Everything between them happens while the main thread is free to run other code.

The Thread Pool Has Four Threads

The thread pool explains a behavior that surprises many developers: some operations run in parallel, but only up to a limit. Besides file system calls, the thread pool also runs dns.lookup, the asynchronous compression functions of the zlib module, and the slow hashing functions of the crypto module. The pool has four threads by default.

This script starts five password hashes, each of which keeps one thread busy for about a third of a second:

const crypto = require('crypto'); const start = Date.now(); for (let i = 1; i <= 5; i++) { crypto.pbkdf2('secret', 'salt', 1000000, 64, 'sha512', () => { console.log(`Hash ${i} done after ${Date.now() - start} ms`); }); } console.log('Five hashes started');
Five hashes started Hash 3 done after 343 ms Hash 1 done after 347 ms Hash 4 done after 348 ms Hash 2 done after 352 ms Hash 5 done after 681 ms

Four hashes finish together, because each one had its own thread. The fifth had to wait for a free thread, so it finished after twice the time. The order of the first four also varies from run to run, since they finish at almost the same moment.

You can change the size of the pool with the UV_THREADPOOL_SIZE environment variable before the program starts. With a value of 5, all five hashes in the same script finished between 364 ms and 429 ms on the same computer. A larger pool helps only when the computer has processor cores free to run the extra threads.

Built-in Modules

V8 provides the language, and libuv provides the I/O. The built-in modules are the JavaScript interface between your code and those parts. They come with every Node.js installation, so you load them with require and install nothing. These five appear throughout the course:

  • fs reads and writes files.
  • http creates web servers and sends HTTP requests.
  • path builds and takes apart file and folder paths.
  • os reports details about the operating system and the hardware.
  • events lets your own objects emit events and register listeners for them.

npm Comes with Node.js

npm, the Node Package Manager, is not part of the runtime itself; it is a separate program that the Node.js installers include. It downloads open-source packages from a public registry into a node_modules folder in your project, and it records which packages the project needs in a file called package.json. When your code calls require with the name of a package, Node.js looks for it in node_modules.

Why the Runtime Matters When You Write Code

Knowing which part does the work tells you what is cheap and what is expensive. Waiting for the network costs no thread at all. File operations, dns.lookup and the slow crypto functions share four threads, so a server that starts hundreds of them at once makes most of them wait in line. Calculation in your own JavaScript runs on the main thread, and nothing else in the process runs until it finishes.

Reading Progress

0%


Vote for new content

On This Page

What Changes from the Browser

The Parts of the Runtime

The Thread Pool Has Four Threads

Built-in Modules

npm Comes with Node.js

Why the Runtime Matters When You Write Code