userland.rundocs70Try the demo
SDK

Node.js

Run Node.js v25.4.0 programs in the page: on the browser's JS engine for speed, or as the real RISC-V binary for fidelity, with NodeRuntime snapshots to skip V8's boot on repeated runs.

Which runner executes nano.node() is set by engines. This page boots with engines: { node: "host" }, so the plain samples run on the host engine and start in milliseconds.

Node on the host engine

Node.js

Built-in modules

events, Buffer, crypto hashes, timers and microtasks behave as in Node. Upstream events, path, assert, querystring, punycode and string_decoder run verbatim.

javascript
const { EventEmitter } = require("events");
const crypto = require("crypto");
const bus = new EventEmitter();
bus.on("ready", (v) => console.log("event", v));
bus.emit("ready", 42);
console.log(Buffer.from("nano").toString("base64"));
console.log(crypto.createHash("sha256").update("nano").digest("hex").slice(0, 12));
setTimeout(() => console.log("timer fired"), 10);
Promise.resolve().then(() => console.log("microtask"));
loading…
Expected output
event 42
bmFubw==
f7a5936c485e
microtask
timer fired
Node.js

fs on the shared filesystem

The program's fs is the VM's filesystem, so BusyBox and other runners see what it writes.

javascript
const fs = require("fs");
fs.mkdirSync("/work/out", { recursive: true });
fs.writeFileSync("/work/out/report.json", JSON.stringify({ ok: true, at: "/work/out" }));
console.log(fs.readdirSync("/work/out"), JSON.parse(fs.readFileSync("/work/out/report.json", "utf8")).ok);
loading…
Expected output
[ 'report.json' ] true

Server samples (http.createServer) are on Serve mode. What the host engine does not implement yet (zlib, child_process without a VM delegate, vm contexts, http2, ESM entry files, node:sqlite in the browser) is listed under Limitations.

Node on the RISC-V runner

With engines.node: "vm" (the default) node() runs the upstream Node.js ELF on the emulator. Install it with installApp("node") and give the VM ramMB: 1800. The sample is on Runners & engines.

NodeRuntime snapshots

On the RISC-V runner most of a short Node run is V8 booting. nano.nodeRuntime() boots V8 once, snapshots the VM at a sentinel, and restores that snapshot for every run, so each run is isolated (it starts from the same state) and skips the boot.

OptionTypeDefaultDescription
warmup(opts?)Promise<void>maxSteps 2e9Boot Node and capture the snapshot. Concurrent callers share one warmup.
run(source, opts?)Promise<ExecResult>—Restore the snapshot and evaluate source (as the body of a Function, so use process.mainModule.require instead of require). Warms up first if needed.
run opts.extraFilesArray<{ path, content }>—Files written into the restored VM before the run (per-run inputs).
run opts.onData / maxSteps2e9As in ExecOptions.
reset()void—Drop the snapshot; the next run warms up again (from the current files).
isWarmboolean—Whether a snapshot is held.
SDK · JavaScript

warmup, run, extraFiles, reset

Boots a slim VM with BusyBox and Node.js from the catalog (about 63 MB the first time), warms a snapshot, then runs three scripts against it. The second script sets a global; the third doesn't see it, because each run restores the same snapshot. Timings in the expected output are from Chromium on an Apple M3 Max.

javascript
const vm = await sdk.createNano({ image: { wasm: "/nano/nano.wasm" }, ramMB: 1800 });
await vm.installApp("busybox");
await vm.installApp("node");
const rt = vm.nodeRuntime();
let t0 = performance.now();
await rt.warmup();
print("warm:", rt.isWarm, "after", Math.round(performance.now() - t0), "ms");
t0 = performance.now();
const r = await rt.run("console.log(process.mainModule.require('fs').readFileSync('/tmp/in.json', 'utf8'))", {
  extraFiles: [{ path: "/tmp/in.json", content: '{"value":42}' }],
});
print(r.stdout.trim(), "in", Math.round(performance.now() - t0), "ms");
await rt.run("globalThis.leak = 1");
print("isolated:", (await rt.run("console.log(typeof globalThis.leak)")).stdout.trim());
rt.reset();
print("after reset:", rt.isWarm);
vm.destroy();
loading…
Expected output
warm: true after 1489 ms
{"value":42} in 579 ms
isolated: undefined
after reset: false
Caution · Read stdout, not exitCode, after a warm run: Node's shutdown can exit 134 after printing correct output. The catalog's node recipe marks 134 as benign for provision().
Caution · NodeRuntime needs a separate Node ELF. On console.wasm, which embeds only BusyBox, SDK 0.2.10 snapshots the embedded BusyBox instead and warmup() fails with "App did not reach the snapshot sentinel within budget". Use the slim nano.wasm with installApp("node") or image.node, as above.

Where Node runs, compared

host engine
RISC-V runner
RISC-V + NodeRuntime
Start-up
milliseconds
V8 boot on the emulator (seconds)
one warmup, then fast restores
process.arch
x64
riscv64
riscv64
Built-ins
reimplemented subset
upstream Node.js v25.4.0
upstream
Memory
the page VM
ramMB ≈ 1800
ramMB ≈ 1800
State between runs
fresh process each run
fresh process each run
restored snapshot each run

For complete Node apps (npm tools, dev servers), see Node.js in the browser and the hello-node example.