zRun — a nano JavaScript runtime for Zig

zRun

zRun is a compact JavaScript runtime and source compiler written in Zig. The current implementation is about 11K lines of Zig.

I built it to separate application business logic from systems code in a large open-source project.

The intended model is:

TypeScript business logic
        ↓
      zRun
        ↓
    Zig plugins
        ↓
performance-critical code

Business rules can stay in TypeScript, while networking, storage, heavy computation and other performance-sensitive operations remain native Zig code.

The motivation came from my previous project, Cruller. Cruller started from Bun, but over time I concluded that stripping down a large general-purpose runtime was the wrong direction for this use case. I wanted a much smaller execution layer that I could fully understand and control.

For comparison:

Runtime Approx. source size
V8 ~2,000,000 LOC
QuickJS ~79,871 LOC
MQuickJS ~30,950 LOC
zRun ~11,375 LOC

zRun supports the JavaScript features needed by the target workloads, including closures, classes, exceptions, async/await suspension and resume, collections, BigInt, regular expressions and common standard APIs.

It can compile JavaScript into a versioned bytecode format and execute that bytecode in a runtime that does not need to include the source compiler.

Async execution is host-driven: await can suspend the VM, return control to Zig, and later resume execution when the host operation completes. There is no built-in event loop that the embedding application has to adopt.

The runtime also supports resident compiled modules so bytecode can remain decoded in memory and functions can be called directly by module and function name.

One useful milestone is that zRun can already run Preact SSR. This means the same small runtime can potentially replace a much heavier Bun-based layer for server-side rendering while keeping the application architecture centered around Zig.

Current benchmark results put zRun in roughly the same performance range as MQuickJS and QuickJS on several workloads:

Benchmark Reference / zRun
MQuickJS, same bytecode 0.92x
MQuickJS CLI bytecode 0.87x
QuickJS CLI 0.91x

Values below 1.0 mean zRun is slower in those tests. Other workloads currently show larger gaps. The repository includes the benchmark code and methodology.

The main thing I learned from the project is that a JavaScript runtime can remain quite small if full JavaScript compatibility is not the goal. For this project JavaScript is primarily an application-level language sitting above native Zig functionality.

zRun is intended to become the runtime foundation for Cruller v2.

Supported Zig versions

  • Zig 0.16.0

AI / LLM usage disclosure

I used OpenAI Codex and Luna 6 during development for code generation, refactoring, debugging, tests and implementation experiments.

The runtime architecture, execution model, project direction and benchmark design were defined by me. Generated code was compiled, reviewed, tested and benchmarked as part of the development process.

5 Likes

So, if I am presuming right, this will allow us to have a nodejs compatible runtime in a small package that we can embed anywhere we like?

Yes, that’s roughly the idea. zRun is intended to be a very small embeddable JavaScript runtime that you can integrate almost anywhere.

The important limitation is that you need to stay within a relatively simple subset of JavaScript. For example, I would avoid more complex constructs such as Promise, although async/await itself works.

The idea is to use JavaScript for business logic, lightweight SSR, routing, transformations, cli, and similar high-level tasks, while pushing anything heavy or performance-critical into Zig plugins.

But the main reason I’m building it is extensibility. I want the community to be able to extend a project’s business logic very quickly using tiny compressed modules, potentially around 10 KB each. These modules can be connected through meta-configuration and referenced using content-based addressing.

That makes the model especially interesting for decentralized environments such as IPFS: modules can be distributed independently, identified by their content, composed into larger applications, and loaded only when needed.

For many projects this can be much more efficient than embedding a full Node.js runtime. It also solves a practical problem I had with Cruller, where the previous runtime approach could accumulate memory leaks over time.

I ported the engine to Zig 0.17.

3 Likes

Nice. Can you share a document for features that are not compatible with your library?

That was my mistake. I added tags to the README, and because of that all the blocks showing language constructs were not visible on GitHub. I’ve fixed it now.

Also, during the initial compilation stage, TypeScript is processed with Bun before being compiled to bytecode. There is a verifier at this stage: any unsupported language constructs are reported, and the compilation fails with an error.

1 Like