The engine decision

Everything else in bext-lite hangs on one choice: which JS engine evaluates the route bundle and calls __bextPrismRender. There is no single answer. It is per-target, and it is forced by two hard external facts, not by preference.

The two constraints#

iOS forbids JIT for third-party apps. Only Apple's own WKWebView (JavaScript Core) has the JIT entitlement. A JIT engine like V8 cannot run inside a third-party iOS app. A pure interpreter can. QuickJS is a pure interpreter with no JIT, so it is App Store legal by construction - the single constraint that makes on-device runtimes painful is the one thing this design engineered around on day one.

You cannot embed V8 in a WASM module. Edge platforms (Cloudflare Workers, Fastly Compute) either are someone else's V8/JSC that you do not control, or are WASM where only an interpreter fits. The same is true in the browser. So anywhere you need to ship your own engine to WASM, that engine is QuickJS (via Javy/WASI), not V8.

The per-target answer#

Target Engine Why
Desktop / single-binary server V8 (default), QuickJS optional Already works; React via snapshot proven; footprint fine on a desktop
IoT / ultra-low footprint QuickJS Under 1 MB vs 124 MB; no 4 GB pointer cage; runs on ARM
iOS on-device render QuickJS V8 JIT is banned
Android on-device render QuickJS Uniformity with iOS
Edge (Workers / Fastly) QuickJS on WASM Cannot ship V8; platform V8 is not usable for this bundle model
Browser / service worker QuickJS on WASM Fully-client and offline apps

The QuickJS path is not the fallback - it is the strategically essential one. It is the only road to iOS, edge, and WASM. The V8 path is the "it already works, ship desktop fast" road.

Byte-faithful parity#

The reason two engines is tolerable is that they produce the same bytes.

For PRISM's default string-builder rendering, JSX is compiled server-side and there is no React in the isolate - the polyfills are pure JS and the compiled CJS bundle is engine-neutral. This was proven end to end: four real compiled bundles (share-prism, ecosystem-inklura, demo, compare-robots) rendered byte-identical RouteResult JSON on QuickJS and on V8, with zero missing builtins and the polyfills running unchanged.

React-mode pages ("use client", SSR'd with real ReactDOMServer.renderToString) also render correctly on QuickJS. React and ReactDOMServer are pure JS; the V8 startup snapshot is only a performance optimization, not a correctness dependency. The runtime installs a small server-DOM preamble - stub document, navigator, location, and a microtask-backed MessageChannel that react-dom's scheduler grabs at init - all guarded by typeof so the string-builder path is completely unaffected. React 19 SSR was verified under QuickJS, props threaded and useState initial state intact.

Note

The V8-only React snapshot is a startup speed trick. Its own documented fallback is a full eval, which is exactly what QuickJS does. So React mode is not V8-only; it just loses the snapshot's warm-start on QuickJS.

Why V8 stays on desktop and server#

QuickJS wins on footprint and legality; V8 wins on raw render throughput and the React snapshot. On a desktop or a single-binary server there is no JIT ban and no WASM constraint, the footprint is acceptable, and the V8 render path already exists and is proven in production. So desktop and server default to V8 and treat QuickJS as optional. The moment you target iOS, the edge, the browser, or a tiny ARM box, that calculus flips and QuickJS is the only option that runs at all.

One caveat: streaming#

Suspense streaming SSR (the async-iterator path) is the riskiest sub-feature to carry across engines, and it is unnecessary for a single-tenant local app. bext-lite renders fully and then serves - buffered, not streamed. This is correct for single-tenant and sidesteps the one fragile corner of React-on-QuickJS.