Skip to content

JavaScript

Checked against 2994ef5 on 2026-11-10 by Capy (review pass: Subagent 124 and fable). The files it covers are unchanged since.

A niso page is ordinary JavaScript: a classic script or ES modules, and whatever a bundler makes of Svelte or another framework. The page’s DOM, fetch, timers and the rest of the Web APIs are niso’s, the same on every engine, because every engine runs the same glue over the same core; the engine supplies only the language. Which engine runs the page depends on where it runs:

Engine Where it runs What it’s for
QuickJS (QuickJS-ng) every host (iOS and the other Apple platforms, Android, desktop) by default, and niso-run the default: an interpreter, small, starts fast, loads scripts at run time
Porffor the Apple and Android hosts with NISO_ENGINE=porffor, and niso-run-porffor compiling the page ahead of time into the app, since iOS forbids JIT
Node-API: V8 under Node and Deno, JavaScriptCore under Bun niso-node, headless, in your runtime’s process JIT speed and the runtime’s own WebAssembly, without shipping an engine

Planned: DECISIONS names Static Hermes, to be evaluated against Porffor for shipping, and JIT engines embedded in the hosts: V8 on desktop and Android, and JavaScriptCore on Apple platforms. Neither is on main.

The hosts run one script per page. examples/build.mjs builds every directory in examples/ with a main.js into examples/dist/<name>.js, the script a host runs, compiling Svelte and bundling with esbuild, and examples/dist/<name>.html, the same page for a browser. A host can also load an HTML page with its own scripts and modules (--page on the Apple and iced hosts, niso-run and niso-node; the Android host runs a bundle only). Native modules are imported as niso:<name> (Native modules).

Terminal window
(cd examples && pnpm install --frozen-lockfile && node build.mjs)

QuickJS-ng is the engine every host embeds unless told otherwise, and the one niso’s tests run on. It’s an interpreter, so pages load at run time and the dev loop has no compile step; niso adds WebAssembly through wasmi. Its headless runner is niso-run:

Terminal window
cargo build --release -p niso-quickjs --bin niso-run
target/release/niso-run --tap 60,140 --dump examples/dist/counter.js
<html><head><style id="svelte-rlu2i4">body {margin:0;font-family:system-ui;color:#1c1c1e;background:white;}main.svelte-rlu2i4 {padding:16px;}h1.svelte-rlu2i4 {font-size:34px;margin:0 0 4px;}.lede.svelte-rlu2i4 {color:#6c6c70;margin:0 0 16px;}.card.svelte-rlu2i4 {background:#f2f2f7;border:1px solid #d1d1d6;border-radius:12px;padding:16px;}button.svelte-rlu2i4 {font:inherit;font-size:17px;color:white;background:#007aff;border:none;border-radius:10px;padding:10px 16px;}ul.svelte-rlu2i4 {margin:12px 0 0;padding-left:20px;}li.svelte-rlu2i4 {margin:4px 0;}</style></head><body><main class="svelte-rlu2i4"><h1 class="svelte-rlu2i4">niso</h1> <p class="lede svelte-rlu2i4">A Svelte page, drawn by the platform.</p> <div class="card svelte-rlu2i4"><button class="svelte-rlu2i4">Tapped 1 time</button> <ul class="svelte-rlu2i4"><li class="svelte-rlu2i4">Lay out with blitz</li><li class="svelte-rlu2i4">Draw with the platform</li><li class="svelte-rlu2i4">Tap number 1</li></ul></div></main></body></html>

Getting started goes through niso-run’s options, and Headless testing uses it to test pages.

Porffor compiles JavaScript ahead of time to C. niso’s Porffor binding builds the page’s scripts, niso’s glue and Porffor’s runtime into one program, so the app carries no JS source and no interpreter, which is what iOS’s no-JIT rule asks for. The cost is that a build runs only the scripts it was built with:

  • No module scripts, and no eval of code the build didn’t see.
  • One engine per process, so no workers: new Worker() throws NotSupportedError.
  • Porffor’s WeakRef holds its target and its FinalizationRegistry never calls back, so a node JS once touched is never freed. This and Porffor’s lack of an embedding API are why it isn’t the shipping engine yet.

The Porffor notes list the rest, with repros, and measure startup, frame time and size against QuickJS. To build niso-run on Porffor with one page compiled in (it needs porf on the PATH, or named by PORF):

Terminal window
NISO_PORFFOR_SCRIPTS=examples/dist/counter.js cargo build --release -p niso-porffor
target/release/niso-run-porffor --dump --tap 60,140 examples/dist/counter.js
<html><head><style id="svelte-rlu2i4">body {margin:0;font-family:system-ui;color:#1c1c1e;background:white;}main.svelte-rlu2i4 {padding:16px;}h1.svelte-rlu2i4 {font-size:34px;margin:0 0 4px;}.lede.svelte-rlu2i4 {color:#6c6c70;margin:0 0 16px;}.card.svelte-rlu2i4 {background:#f2f2f7;border:1px solid #d1d1d6;border-radius:12px;padding:16px;}button.svelte-rlu2i4 {font:inherit;font-size:17px;color:white;background:#007aff;border:none;border-radius:10px;padding:10px 16px;}ul.svelte-rlu2i4 {margin:12px 0 0;padding-left:20px;}li.svelte-rlu2i4 {margin:4px 0;}</style></head><body><main class="svelte-rlu2i4"><h1 class="svelte-rlu2i4">niso</h1> <p class="lede svelte-rlu2i4">A Svelte page, drawn by the platform.</p> <div class="card svelte-rlu2i4"><button class="svelte-rlu2i4">Tapped 1 time</button> <ul class="svelte-rlu2i4"><li class="svelte-rlu2i4">Lay out with blitz</li><li class="svelte-rlu2i4">Draw with the platform</li><li class="svelte-rlu2i4">Tap number 1</li></ul></div></main></body></html>

The output is byte for byte the same as QuickJS’s. The build is slow: the C compiler takes minutes over the one C file Porffor writes for the glue and the page, which is why the dev loop stays on QuickJS.

In the hosts, NISO_ENGINE=porffor does the same for the app: NISO_ENGINE=porffor hosts/apple/build.sh counter or NISO_ENGINE=porffor hosts/android/build.sh counter. Not run for this page.

crates/niso-napi is the JS binding as a Node-API addon. The page’s DOM runs in your runtime’s process and its scripts in a realm of their own (a node:vm context), so they get V8’s or JavaScriptCore’s JIT and WebAssembly. The page sees niso’s globals and none of the runtime’s: no process, require or Buffer. That realm is for correctness, not isolation; it’s no security boundary, so don’t run untrusted pages this way.

niso-node is niso-run on this binding and takes the same options, plus --live, which runs the page in real time from the runtime’s event loop until it has nothing left to do:

Terminal window
cargo build --release -p niso-napi
node --experimental-vm-modules bindings/js/napi/niso-node.js --dump --tap 60,140 examples/dist/counter.js
<html><head><style id="svelte-rlu2i4">body {margin:0;font-family:system-ui;color:#1c1c1e;background:white;}main.svelte-rlu2i4 {padding:16px;}h1.svelte-rlu2i4 {font-size:34px;margin:0 0 4px;}.lede.svelte-rlu2i4 {color:#6c6c70;margin:0 0 16px;}.card.svelte-rlu2i4 {background:#f2f2f7;border:1px solid #d1d1d6;border-radius:12px;padding:16px;}button.svelte-rlu2i4 {font:inherit;font-size:17px;color:white;background:#007aff;border:none;border-radius:10px;padding:10px 16px;}ul.svelte-rlu2i4 {margin:12px 0 0;padding-left:20px;}li.svelte-rlu2i4 {margin:4px 0;}</style></head><body><main class="svelte-rlu2i4"><h1 class="svelte-rlu2i4">niso</h1> <p class="lede svelte-rlu2i4">A Svelte page, drawn by the platform.</p> <div class="card svelte-rlu2i4"><button class="svelte-rlu2i4">Tapped 1 time</button> <ul class="svelte-rlu2i4"><li class="svelte-rlu2i4">Lay out with blitz</li><li class="svelte-rlu2i4">Draw with the platform</li><li class="svelte-rlu2i4">Tap number 1</li></ul></div></main></body></html>

Under Deno, run it from its directory with deno run -A niso-node.js …; under Bun, bun bindings/js/napi/niso-node.js …. The addon is found through NISO_NAPI, or else as the newest build under target/.

Not every runtime version can host a page. niso.js checks when it loads, and new Page() throws with the reason when the runtime would leak its own realm into the page:

Runtime Pages
Node 20.19.5+, 22.17.0+, 24.2.0+ with --experimental-vm-modules yes (the script’s shebang passes the flag)
older Node, or Node without the flag refused
Deno 2.9.8 yes
Deno 2.9.5 pages run (the counter above did); the binding’s worker tests hang
Bun 1.4 yes
Bun before 1.4 refused

Bun 1.1.29 refuses, as documented; on the machine used for this page it printed:

Error: niso: no page can be made here: this runtime's vm contexts have a global object whose prototype cannot change, so it cannot be made a Window

From your own code, bindings/js/napi/niso.js exports Page, which niso-node is built on: make one, load HTML or evaluate scripts, run it in virtual time or live() from the loop, render frames and tap. The Node-API notes cover the realm, the event loop, workers, WebAssembly and how WPT results differ from QuickJS’s.

Not on main yet. The branch node-windows adds a window feature to niso-napi that opens pages in real windows from plain Node, drawn by the iced host’s library, with a Window class modeled on Electron’s BrowserWindow. Its notes (node-windows.md on that branch) give these commands, which don’t work on main (there’s no window feature and no --window option):

Terminal window
cargo build --release -p niso-napi --features window
node --experimental-vm-modules bindings/js/napi/niso-node.js --window --html page.html

On main, a page in a desktop window runs on QuickJS in the iced host (Getting started).