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.ccreads the file list fromnode.gyp'slibrary_filesGYP variable.- It emits
node_javascript.cccontaining C++ string literals for every JS source plus a small lookup table. src/node_builtins.ccexposes that table viaBuiltinLoader, 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 globalSymboltable). - "Uncurrying" prototype methods so
ArrayPrototypeMap(arr, fn)is the equivalent of the user callingarr.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
BindingDatato serialize its persistent state. - Asking each reachable
BaseObjectto serialize via itsSerializecallback. - 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 insidemksnapshot. 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.jsor a regular module that lazy-loads on first use. - New
BindingDatamust implementSerialize/Deserializeif it is reachable from the principal Realm at snapshot time (which it almost always is). - New
BaseObjects that exist at snapshot time (like theprocessitself) needIsSnapshotable() == trueand the rightSerializecallbacks.
Entry points for modification
- New embedded module? Add to
node.gyp'slibrary_files, write the source underlib/, document atdoc/api/. - New primordial? Almost never needed. The walk in
lib/internal/per_context/primordials.jsis exhaustive. - Snapshot bug? Look at the
Serialize/Deserializeof the binding involved, thensrc/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.