userland.rundocs70Try the demo
Internals

Performance

The RISC-V runner interprets every guest instruction, so code on it runs much slower than native; the host-engine and wasm runners run at JIT and native-wasm speed. Here are measured numbers and how to stay fast.

Measure it in your browser

SDK · JavaScript

Instructions per second, here

Runs a shell loop on the emulator and divides the guest instructions retired (raw.instructionCount()) by wall-clock time. The counter overstates block-cached instructions by up to about 25% (see below), so treat it as an upper bound. The expected output was recorded in Chromium on an Apple M3 Max.

javascript
const raw = nano.raw;
const t0 = performance.now();
const i0 = raw.instructionCount();
const r = await nano.shExec("i=0; while [ $i -lt 20000 ]; do i=$((i+1)); done; echo $i", { maxSteps: 1e10 });
const ms = performance.now() - t0;
const insns = raw.instructionCount() - i0;
print(r.stdout.trim(), "iterations:", Math.round(insns / 1e6), "M guest instructions in", Math.round(ms), "ms ≈", Math.round(insns / ms / 1000), "MIPS");
loading…
Expected output
20000 iterations: 527 M guest instructions in 1748 ms ≈ 301 MIPS
SDK · JavaScript

The same loop on three runners

Ten million additions in a JS loop on the host engine and on the Boa interpreter, and 20,000 iterations of a shell loop on the RISC-V runner. Your timings will differ; the expected output was recorded in Chromium on an Apple M3 Max.

javascript
let t = performance.now();
await nano.node(["-e", "let s = 0; for (let i = 0; i < 1e7; i++) s += i; console.log(s)"]);
print("host-engine Node, 1e7 iterations:", Math.round(performance.now() - t), "ms");
t = performance.now();
await nano.script("let s = 0; for (let i = 0; i < 1e6; i++) s += i; s", { limits: { loopIterations: 2e6 } });
print("Boa, 1e6 iterations:", Math.round(performance.now() - t), "ms");
t = performance.now();
await nano.shExec("i=0; while [ $i -lt 20000 ]; do i=$((i+1)); done", { maxSteps: 1e10 });
print("BusyBox sh on RISC-V, 2e4 iterations:", Math.round(performance.now() - t), "ms");
loading…
Expected output
host-engine Node, 1e7 iterations: 85 ms
Boa, 1e6 iterations: 286 ms
BusyBox sh on RISC-V, 2e4 iterations: 1803 ms

Node.js on the emulator

Booting Node.js v25.4.0 and running console.log(1) retires about 505 million guest instructions. With the command-line runner (node test/run.mjs runners/riscv/images/node --cmd node -e …) on an Apple M3 Max under Node.js 24.11.1 that took 1.96 s and 2.04 s wall-clock in two runs, including the host's own start-up. In a browser tab the first run also downloads the 62 MB ELF from the catalog (cached in OPFS afterwards).

Slowdown by workload

From the repository's benchmark suite (bench/, recorded in bench/PERFORMANCE.md): each benchmark run with native Node.js and with the RISC-V Node.js ELF on NanoVM, Apple Silicon. These are the baseline numbers, before the block-engine rework below.

Benchmark
Native
NanoVM
Slowdown
http-throughput
21 ms
990 ms
47×
object-create
16 ms
1,156 ms
72×
json-parse
64 ms
6,501 ms
102×
string-ops
6 ms
1,218 ms
203×
array-sort
23 ms
4,791 ms
208×
crypto-hash
7 ms
1,750 ms
250×
regex
29 ms
8,013 ms
276×
fib
265 ms
156,081 ms
589×
buffer-ops
38 ms
29,508 ms
777×
math-compute
13 ms
17,194 ms
1,323×
loop-compute
40 ms
64,113 ms
1,603×
total
522 ms
291,315 ms
558×

The block-engine rework (block dispatch at the top of exec(), superblocks across forward branches, FP/atomics inside blocks, a 16,384-entry block cache) raised block coverage on loop-compute from 0.2% to 100% and cut its wall-clock time by 13% (67.3 s → 58.2 s); math-compute went from 17.8 s to 15.4 s. The counters behind "MIPS" over-count block instructions by about 25%, so wall-clock time is the number to trust.

Staying fast

  • Run Node.js on the host engine unless you need exact Node behavior; it starts in milliseconds.
  • Use NodeRuntime or provision() on the RISC-V runner: one V8 boot, then snapshot restores.
  • Prefer wasm builds of tools when they exist; they run on the wasm runner at native wasm speed.
  • Host the VM in a Web Worker so long runs don't block the page.
  • Bound untrusted work with maxSteps and script limits.

How this compares to other in-browser runtimes: comparisons.

The emulator is a good fit for editors, linters, type checks, small scripts, previews and teaching. It is not a production compute runtime.