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.
Building pages
Section titled “Building pages”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).
(cd examples && pnpm install --frozen-lockfile && node build.mjs)QuickJS
Section titled “QuickJS”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:
cargo build --release -p niso-quickjs --bin niso-runtarget/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
Section titled “Porffor”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
evalof code the build didn’t see. - One engine per process, so no workers:
new Worker()throwsNotSupportedError. - Porffor’s
WeakRefholds its target and itsFinalizationRegistrynever 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):
NISO_PORFFOR_SCRIPTS=examples/dist/counter.js cargo build --release -p niso-porffortarget/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.
Node, Deno and Bun
Section titled “Node, Deno and Bun”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:
cargo build --release -p niso-napinode --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 WindowFrom 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.
Windows from Node
Section titled “Windows from Node”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):
cargo build --release -p niso-napi --features windownode --experimental-vm-modules bindings/js/napi/niso-node.js --window --html page.htmlOn main, a page in a desktop window runs on QuickJS in the iced host (Getting started).