The best WebAssembly runtime may still be no runtime at all

In 2023, I wrote that the best WebAssembly runtime may be no runtime at all.

To summarize: if you already have a WebAssembly module, translating it to C and compiling that C with a normal native compiler can be surprisingly hard to beat.

I expected that argument to get weaker over time, because WebAssembly runtimes keep improving. Their compilers have better register allocators, better lowering, better support for newer WebAssembly instructions, and more deployment polish than a small transpiler can reasonably have.

Then I ran the 2026 benchmark.

The 2026 WebAssembly benchmark has the full table for other runtimes.

And the WebAssembly-to-C path performed very well, but was beaten by new runtimes implementing the “wide arithmetic” WebAssembly proposal.

I was curious to see how the WebAssembly-to-C approach would work with if that proposal was implemented.

So, let’s compare:

  • Wasmer 7.1.0
  • Wasmtime 46.0.0
  • WABT wasm2c

For wasm2c, I added support for wide arithmetic, which was surprisingly trivial to implement. You can find my fork here.

The generated C code simply uses compiler carry intrinsics and C’s 128-bit integer type.

Note that I compiled the generated C with WABT’s Segue memory mode enabled, with zig cc -O3 -march=native, on the exact same libsodium benchmark suite as the previous runtime post.

Every build uses the lime1+simd128+wide_arithmetic feature set.

Oh, and before you ask: wasm2c supports memory protection using guard pages, like other runtimes.

The speed numbers

The numbers below are slowdowns relative to the native libsodium build. A value of 1.25x native means the benchmark took 25% more time than native code.

Lower is better.

wasm2c with wide arithmetic, ranked by geomean slowdown

The median is a tie with Wasmer. But the wasm2c executable used 0.887x as much time as Wasmer on the geomean, and 0.814x as much as Wasmtime.

The dumb C path survived the feature upgrade. Once the new arithmetic instructions were added, a normal C compiler could again produce code competitive with dedicated WebAssembly compilers.

Memory usage

I also measured peak resident set size with /usr/bin/time -v.

This is cold process RSS for one command invocation, using the median of fifteen runs. It includes the runtime executable, its startup state, JIT or compilation machinery used by that command, and anything else the process maps while running the benchmark.

Note that Wasmer and Wasmtime have a fixed cost that can be amortized if a service keeps the runtime alive and runs many modules or many calls.

Cold process memory, median of 15 runs

Most of that difference is fixed engine overhead. The module itself costs very little.

If I subtract the RSS of an empty WASI module run through the same path, with a 64 MiB maximum linear memory like the benchmark build, only a couple of MiB remain:

What the module itself costs

At run time, with the WebAssembly code directly compiled to C, then to executable code, there’s no engine process to bring along.

No runtime is still a good choice

The generated output is portable C.

So, the deployment target no longer has to be “a platform with a WebAssembly runtime”. It can be any platform with a C compiler and enough libc support for whatever the module imports.

It also means the compiler is a choice.

You can compile the generated C with clang or gcc.

But you can also take advantage of modern C toolchains such as Fil-C to get memory safety on wasm64, or on wasm32 without depending on virtual-memory guard pages.

Or you can use a formally verified compiler such as CompCert for high assurance code generation.

I didn’t benchmark those toolchains. The point is that WebAssembly-to-C gives you that option, while a dedicated runtime doesn’t.

This is also why I remain impressed by Wasmer.

Wasmer also implements WASIX, which fills many of the POSIX-shaped gaps that plain WASI still leaves open. So, if your goal is to run existing applications in WebAssembly with fewer rewrites, this is the way to go, not WebAssembly-to-C or other options.

And Wasmtime implements the “Component Model”, that improves code separation and gives applications a typed interface boundary for composing pieces of WebAssembly. I have an intuition that the same thing can be achieved using WebAssembly-to-C by compiling components separately, but I didn’t think about it much.

Regardless WebAssembly-to-C is the wrong answer if you need dynamic loading of untrusted code without precompilation, runtime-level policy, preemption, fuel, component-model machinery, or one engine reused across many tenants.

It’s also the wrong answer if your deployment model depends on shipping one portable .wasm artifact and letting the destination machine choose how to run it.

But if you control the WebAssembly module, can precompile it, and don’t need runtime services, WebAssembly-to-C is very close to a no-brainer, especially with the addition of wide arithmetic.