Open-Source Wikis

/

Node.js

/

Primitives

/

Builtins, primordials, and snapshots

nodejs/node

Builtins, primordials, and snapshots

This page covers three intertwined mechanisms that make Node start fast and stay correct: the builtin JS module loader, the frozen-primordials trick, and the V8 startup snapshot.

Embedded builtins

Every .js and .mjs under lib/ (and selected files under deps/, tools/) is compiled into the node binary at build time. The mechanism:

  • tools/js2c.cc reads the file list from node.gyp's library_files GYP variable.
  • It emits node_javascript.cc containing C++ string literals for every JS source plus a small lookup table.
  • src/node_builtins.cc exposes that table via BuiltinLoader, which the JS bootstrap uses as a virtual filesystem.
graph LR
    libs[lib/**/*.js, deps/undici/...] --> js2c[tools/js2c.cc]
    js2c --> nodejscc[node_javascript.cc]
    nodejscc --> bin[node binary]
    bin --> Loader[BuiltinLoader at runtime]
    Loader --> Compile[CompileFunction in V8]
    Compile --> Cache[Code cache]
    Cache --> Run[execute]

Because the source is bundled, process.moduleLoadList reports BuiltinModule fs etc. without ever touching the disk. --node-builtin-modules-path=$(pwd) short-circuits this and reads from disk instead, which is what enables fast iteration on JS-only changes (see Getting started).

There is also a per-module bytecode cache (features.cached_builtins) — the V8 code cache for each builtin is precomputed and embedded so the first require('fs') does not pay parse-and-compile cost.

Primordials

User code can replace Array.prototype.map, String.prototype.startsWith, and other built-in methods. Internal Node code is therefore forbidden from using them directly. Instead, lib/internal/per_context/primordials.js runs first in every Realm and captures pristine references:

const { ArrayPrototypeMap, StringPrototypeStartsWith } = primordials;

The capture works by walking every built-in object and either:

  • Storing the property descriptor verbatim (Math, Number, the global Symbol table).
  • "Uncurrying" prototype methods so ArrayPrototypeMap(arr, fn) is the equivalent of the user calling arr.map(fn), but without using the user-visible prototype access.

Per-Realm, primordials is a frozen object that internal modules can pull from. The capture file uses /* eslint-disable node-core/prefer-primordials */ because it is the one place that genuinely has to call the built-ins by name.

The custom ESLint rule tools/eslint-rules/prefer-primordials.js enforces the rule across the rest of lib/internal/. See Patterns and conventions.

Frozen __proto__: null options bags

A close cousin to primordials: many internal helpers use options objects literal { __proto__: null, foo: 1 } so that a malicious user Object.prototype cannot influence default lookups. This convention is enforced by an additional ESLint rule.

V8 startup snapshot

Running lib/internal/per_context/primordials.js and lib/internal/bootstrap/{realm,node}.js from scratch every Node startup would cost tens of milliseconds. Instead, Node ships a V8 startup snapshot.

Build-time

graph LR
    bs[node binary without snapshot] --> mks[node_mksnapshot]
    perctx[per_context/*.js] --> mks
    rjs[bootstrap/realm.js] --> mks
    njs[bootstrap/node.js] --> mks
    mks --> blob[snapshot blob]
    blob --> bs2[node binary with snapshot]

tools/snapshot/ orchestrates this. node_mksnapshot runs a minimal Node, drives the bootstrap to completion, and writes the V8 isolate's heap to disk. The SnapshotBuilder::Generate() function (src/node_snapshotable.cc) handles the C++ side, including:

  • Asking each BindingData to serialize its persistent state.
  • Asking each reachable BaseObject to serialize via its Serialize callback.
  • Recording the V8 internal-field map so deserialization can re-attach C++ instances.

Run-time

Realm::DeserializeProperties swaps the snapshotted V8 heap into the new Realm. The principal Realm starts already-bootstrapped: process exists, internalBinding exists, the timer machinery is set up. Then lib/internal/process/pre_execution.js runs to do whatever could not be snapshotted (read env vars, set up the inspector, etc.).

Pass --no-node-snapshot to force the slow path; this is mainly useful for debugging.

User snapshots

--build-snapshot lets users build their own startup snapshot from a custom entry. The JS API at v8.startupSnapshot.addDeserializeCallback(fn) and addSerializeCallback(fn) lets user code participate. Same machinery, smaller scale: see systems › single-executable-applications for how SEA uses it.

Implications for new code

  • New code in lib/internal/bootstrap/** will run inside mksnapshot. It must be deterministic (no env access, no async, no current-time, no PRNG).
  • Code that needs runtime state belongs in lib/internal/process/pre_execution.js or a regular module that lazy-loads on first use.
  • New BindingData must implement Serialize/Deserialize if it is reachable from the principal Realm at snapshot time (which it almost always is).
  • New BaseObjects that exist at snapshot time (like the process itself) need IsSnapshotable() == true and the right Serialize callbacks.

Entry points for modification

  • New embedded module? Add to node.gyp's library_files, write the source under lib/, document at doc/api/.
  • New primordial? Almost never needed. The walk in lib/internal/per_context/primordials.js is exhaustive.
  • Snapshot bug? Look at the Serialize/Deserialize of the binding involved, then src/node_snapshotable.cc. Reproduce by toggling --no-node-snapshot.
  • Tests for snapshot machinery: test/parallel/test-snapshot-*.js, test/parallel/test-bootstrap-*.js.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Builtins, primordials, and snapshots – Node.js wiki | Factory