JavaScript engines: QuickJS, JavaScriptCore, V8, Hermes

A Zinc program written in typed Zinc runs on Zinc’s own engines: the interpreter while you work, native code once built (zinc build). A program that wants the JavaScript ecosystem (npm packages, three.js, PixiJS, Phaser, plain .js) runs on a JavaScript engine instead, and Zinc lets you pick which one. The program does not change: the same bundle, the same zinc: modules, the same plugins, WebGL and native modules on all of them.

zinc run main.js                         # QuickJS-ng, built into zinc
zinc run examples/hero --engine jsc      # JavaScriptCore, the one in macOS
zinc run examples/hero --engine v8       # V8, downloaded the first time
zinc run examples/hero --engine hermes   # Hermes, downloaded the first time (experimental)

A project names its engine once in zinc.json, and zinc run, zinc dev, zinc build and zinc export follow it:

{ "name": "my-app", "engine": "v8" }

How one program runs on four engines

Every engine is bound to Zinc through one interface, Node-API (the stable C ABI Node.js, Bun and Deno give native addons). Zinc’s side of each binding is written once over Node-API: the host functions (zinc:gfx, zinc:ui, files, network…), the web environment, WebGL, the native modules of plugins with their callbacks and promises, WebAssembly. An engine only brings its Node-API and a few lines that start it (about a hundred lines of C++ per engine). Calls stay cheap: 50 to 115 ns from JavaScript into Zinc, depending on the engine (table below).

QuickJS is linked into zinc. The other engines are modules zinc loads when a program asks for them: JavaScriptCore’s ships with zinc (the engine itself is the system’s), V8’s and Hermes’s come as engine packs from Zinc’s signed index.

Engine packs: downloaded, checked, kept

The first zinc run --engine v8 (or hermes) downloads the engine’s pack, checks its SHA-256 and the index’s signatures (TUF, the same index as plugins) and that it was built for this zinc, then keeps it in ~/.zinc/engines. WebAssembly on QuickJS comes the same way: the first program that uses WebAssembly downloads the wasm pack (WAMR’s interpreter, 0.1 MB).

zinc engine list               # what is installed, and what is beside zinc
zinc engine list --available   # what the index offers
zinc engine install v8         # ahead of time (offline later, a classroom)
zinc engine update             # the newest pack built for this zinc
zinc engine remove hermes

zinc doctor lists the installed engines; zinc run says once a day when a newer pack exists. ZINC_ENGINE_V8=<path> (or _HERMES, _JSC) runs your own build of a module instead.

Which one to pick

Measured on an Apple M1 Pro (docs/reports/zinc-next-perf.md, 2026-10-21; the machine was shared, take the numbers as orders of magnitude):

QuickJSJavaScriptCoreV8Hermes
How it runs JavaScriptinterpreterJITJITbytecode VM, JIT on arm64
Comes frombuilt into zincmacOSpack, 14 MBpack, 2 MB
Call into Zinc70 ns115 ns52 ns50 ns
fib(30), pure JavaScript75 ms5 ms8 ms32 ms
hero, CPU for 600 frames1.04 s0.94 s0.53 s0.89 s
hero, memory~50 MB~140 MB~80 MB~70 MB
UI scenes of the render suite, per frame1.5 to 3.7 ms0.6 to 1.3 ms0.6 to 0.7 ms0.8 to 3.2 ms
Debugger (--inspect)Safari Web InspectorChrome DevToolsChrome DevTools
Precompiled at zinc build.qbc bytecode.hbc bytecode
WebAssemblywasm packbuilt inbuilt inno
Platforms todayall of Zinc’smacOSmacOS, Apple siliconmacOS, Apple silicon
  • QuickJS (the default for JavaScript): the smallest and the most portable, nothing to download, the least memory. Slower as soon as the program computes a lot in JavaScript.
  • V8: the fastest, close to Zinc’s native build on UI work. The choice for heavy JavaScript (three.js scenes, large apps).
  • JavaScriptCore: fast, nothing to download on a Mac, inspectable in Safari; the most memory.
  • Hermes (experimental): React Native’s engine. Starts fast from its bytecode, small to download, the cheapest calls into Zinc; about QuickJS’s speed on UI work. Its place is on devices where a JIT is not allowed.

Typed Zinc needs none of them: zinc build compiles it to native code, with no JavaScript engine inside.

Debugging

zinc run main.js --engine v8 --inspect        # chrome://inspect lists it (port 9229; --inspect=9230 for another)
zinc run main.js --engine hermes --inspect-brk   # Chrome DevTools, paused on the first line
zinc run main.js --engine jsc --inspect       # Safari > Develop > this Mac lists it

Breakpoints, stepping, the console and your own files (source maps) work as in a browser. zinc dev turns the inspector on for a project whose zinc.json names v8 or hermes.

Building and exporting

zinc build of a project on a JavaScript engine writes the bundle (build/<name>.js and its source map). On QuickJS it also writes build/<name>.qbc and on Hermes build/<name>.hbc: bytecode that zinc run loads without parsing the source (hero starts about 50 ms sooner on QuickJS, about 200 ms on Hermes). A bytecode file older than its bundle, or made by another version of the engine, is ignored and the source runs. zinc export --target macos makes an app that carries the bundle, its bytecode and, for V8 or Hermes, its engine pack.

Limits today

  • V8 and Hermes packs are built for Apple silicon Macs. Intel Macs, Linux, Windows and the boards get them in Zinc’s cross-platform pass; until then --engine v8 there says the engine is not installed and could not be downloaded.
  • Hermes has no WebAssembly and no import() of a module at run time.
  • Static Hermes (shermes, Hermes compiled ahead of time to native code) runs under Zinc as a developer tool (tools/hermes-unit), not a zinc option: reports/hermes-engine.md.

Design and measures: reports/js-engines-jsc-v8.md, reports/hermes-engine.md, the bundler: 11-quickjs-bundle.md.

Edit this page on GitHub ↗