Upgrade my WASM interpreter to Zig0.16, here are my findings

Last month, I shared in the community that I wrote a very fast Wasm interpreter, Wasmz, using Zig.

Recently, I created an experimental zig-0.16 branch and initially implemented it.

I will put the report at the end. First is the conclusion: apart from a significant increase in binary size (795KB → 951KB, +19.6%), there are no other differences that would surprise me.

In terms of performance, I deployed GitHub Actions on an idle AMD 5850u Linux laptop and intentionally set it up as a “clean room” to prevent the “noisy neighbor” effect caused by shared VPS. I have been monitoring system resource usage, so the reported data is definitely reliable.

The binary size increase stems from the new I/O abstraction. In version 0.15.2, I thought I/O would be an optional feature, similar to Python’s async I/O. However, after practice, I found that the new I/O mechanism is tightly coupled with the standard library. Unless I write a large amount of C code and use Zig as a glue-like role, it is almost impossible to avoid introducing I/O in the Wasi implementation. Naturally, Wasmz itself cannot escape the cost of binary bloat.

Another regression is that it is currently impossible to compile Wasmz itself into a wasm file that can run in the browser (sounds a bit like performance art, haha). This is also due to the impact of I/O, but I believe the team will resolve this issue soon :slight_smile:

# wasmz Performance Regression Report (Zig 0.15.2 → Zig 0.16)

**Date:** 2026-05-08  
**Target Runtime:** wasmz (`ReleaseFast`)  
**Platform:** Linux 6.12.57+deb13-amd64 x86_64

## Binary Size

| Zig Version | Size | Change |
|---|---:|---:|
| 0.15.2 | 795.5 KB | — |
| 0.16 | 951.8 KB | +156.3 KB (+19.6%) |

---

# Execution Time Comparison

Lower is better.

## fib(30) — pure C compiled to WASM

| Zig Version | Median | Stddev | Change |
|---|---:|---:|---:|
| 0.15.2 | 30.9 ms | ±0.9 | — |
| 0.16 | 33.0 ms | ±1.0 | +2.1 ms (+6.8%) |

## QuickJS fib(25)

| Zig Version | Median | Stddev | Change |
|---|---:|---:|---:|
| 0.15.2 | 141.2 ms | ±2.6 | — |
| 0.16 | 139.6 ms | ±3.0 | -1.6 ms (-1.1%) |

## esbuild bundling

| Zig Version | Median | Stddev | Change |
|---|---:|---:|---:|
| 0.15.2 | 761.6 ms | ±11.0 | — |
| 0.16 | 780.0 ms | ±12.2 | +18.4 ms (+2.4%) |

---

# Memory Usage Comparison

Lower is better.

## fib(30)

| Zig Version | Peak RSS | Avg RSS | Peak Change | Avg Change |
|---|---:|---:|---:|---:|
| 0.15.2 | 17.5 MB | 8.7 MB | — | — |
| 0.16 | 17.9 MB | 8.9 MB | +0.4 MB (+2.3%) | +0.2 MB (+2.3%) |

## QuickJS fib(25)

| Zig Version | Peak RSS | Avg RSS | Peak Change | Avg Change |
|---|---:|---:|---:|---:|
| 0.15.2 | 1.7 MB | 1.2 MB | — | — |
| 0.16 | 1.8 MB | 1.2 MB | +0.1 MB (+5.9%) | ~0% |

## esbuild bundling

| Zig Version | Peak RSS | Avg RSS | Peak Change | Avg Change |
|---|---:|---:|---:|---:|
| 0.15.2 | 1.7 MB | 1.6 MB | — | — |
| 0.16 | 1.8 MB | 1.5 MB | +0.1 MB (+5.9%) | -0.1 MB (-6.3%) |
2 Likes

After this experiment, I am actually weighing whether to follow bun’s approach and use C to create an Io-related wasmz std, so that I can completely avoid using Zig’s built-in new Io.

Honestly, I really like the abstraction of the new Io. It’s cool that you can change the Io implementation backend by replacing the injected parameter values. But on the other hand, I think projects like Wasmz (a single-threaded interpreter) should not be affected by the size of Io.

The most selfish thought is that I hope to keep both the new Io and the old API implementations as two interfaces (of course I know it’s impossible).

1 Like

Aren’t these two statements contradictory? Would you switch your approach just because of the binary size?

The ~7% performance loss in fib is quite bad and, for the kinds of programs I write, more important than the binary size increase.

At this point, I’m fairly convinced Zig will switch back to comptime interfaces. I can’t read Andrew’s thoughts but, I mean, 0.16 has had the most amount of complaints than any other release that I remember, and most problems, at least the binary bloat, would be easily solved by doing comptime dispatch rather than runtime. And there simply isn’t any argument in favor of runtime dispatch, other than programmer ergonomics, which can definetely be solved (as opposed to the devirtualization problem, which is a much harder problem).

Although I’ve been trying to make Wasmz as fast as possible, in the pure interpreter space, people focus more on binary size and cold start speed. Moreover, rewriting some modules in C can also deliver computational performance improvements.

When I said I’m not surprised, it’s because I believe we’ll definitely find the cause of the Fibonacci regression—after all, this is a very pure case. If it were QuickJS or the WASM version of SQLite showing a significant performance drop, I’d actually be more concerned.

1 Like