Wasm_of_ocaml compiles OCaml bytecode to WebAssembly, targeting WasmGC so that OCaml values are managed by the host garbage collector. This post covers the most relevant Wasm changes in versions 6.1 through 6.3, and three PRs that add WASI support, native effects via Stack Switching, and dynlink/toplevel support. The Background section at the end has a short recap of where the project fits alongside js_of_ocaml.
What changed in 6.1-6.3
The full changelog covers all three releases in detail. Below are the changes most relevant to the Wasm backend.
The compiler writes Wasm binaries directly now (6.1)
Before 6.1, the compiler emitted WAT (WebAssembly text format) and then converted it to binary via Binaryen. Since 6.1, it writes .wasm binary modules directly, removing that conversion step. Binaryen is still a required system dependency. WAT output is still available for debugging and is also faster to generate now.
Better Wasm code generation (6.1-6.3)
Several changes across 6.1-6.3 improve the quality of the generated Wasm code:
- Direct calls for known functions (6.1). When the compiler can determine the target of a function call at compile time, it emits a
call_refinstruction. Having context on which functions are called where enables the Binaryen toolchain to better optimize the generated Wasm code. - Precise closure environment types (6.1). Closures carry refined WasmGC struct types for their captured variables instead of generic arrays. This gives the Wasm engine exact layout information.
- Closure code pointer omission (6.2). When a closure is never called indirectly through a generic apply, the code pointer field is omitted from the closure struct entirely.
- Number unboxing (6.3). The compiler tracks unboxed int/float values through chains of arithmetic operations and keeps them in Wasm locals instead of allocating heap boxes. This is the kind of optimization that makes tight numerical loops actually fast.
- Reference unboxing (6.3).
refcells that don't escape a function become Wasm locals instead of heap-allocated mutable GC structs. - Specialized comparisons and bigarray ops (6.3). Instead of dispatching through
caml_compareor generic bigarray accessors, the compiler emits type-specific Wasm instructions when it can resolve types statically.
There are also shared compiler improvements (benefiting both js_of_ocaml and wasm_of_ocaml): the inlining pass was rewritten in 6.1 (#1935, #2018, #2027) with better tailcall optimization, deadcode elimination, and arity propagation between compilation units.
Runtime additions (6.1-6.3)
- Marshal now handles zstd-compressed values in the Wasm runtime (6.1, fix in 6.3).
- Bigarray element access uses
DataViewget/set, which is faster than the typed array primitives that we used before (6.1). - Effect handler continuation resumption is more efficient (6.1).
Unix.timesworks in the Wasm runtime (6.3).Dom_html.onloadadded for Wasm-compatible load event handling -- the JSwindow.onloadpattern doesn't work when Wasm modules load asynchronously (6.3).
Compatibility
6.1 dropped OCaml 4.12 and earlier, requires Dune 3.19, and added preliminary OCaml 5.4 support. The compiler runs on Node.js 22+ (which has WasmGC support), CloudFlare Workers (V8 12.0+), and WasmEdge 0.14.0+.
Three Features in Flight
The three PRs below address the three biggest limitations of wasm_of_ocaml relative to js_of_ocaml: no standalone execution outside the browser, CPS overhead for effects, and no dynlink/toplevel. Dynlink/toplevel (#2187) was merged on 2026-04-08; the other two are still open.
WASI support (PR #1831)
Until now, wasm_of_ocaml has been browser-oriented: the generated Wasm module expects a JavaScript host to provide I/O, filesystem access, and so on. WASI (WebAssembly System Interface) is a standard set of system-call-like imports that lets Wasm modules run on standalone runtimes without a JS host.
This PR by Jerome Vouillon adds a --enable wasi flag:
wasm_of_ocaml --enable wasi foo.byte -o foo.js
The output still follows the existing convention (a .js file and a .assets/ directory with the .wasm code), but the Wasm file can now also be run directly on standalone runtimes. On the Wizard engine (which supports legacy exception handling):
wizeng.x86-64-linux -ext:stack-switching -ext:legacy-eh foo.assets/code.wasm
For wasmtime and other runtimes using the newer exnref-based exception handling, compile with --enable exnref:
wasm_of_ocaml --enable wasi --enable exnref foo.byte -o foo.js
wasmtime -W=all-proposals=y foo.assets/code.wasm
The --enable exnref flag selects the newer exnref-based exception handling spec rather than the legacy proposal used by V8/Chrome and the Wizard engine.
The implementation is substantial: a WASI-compatible filesystem (fs.wat), Unix API bindings covering file operations, process info, time, and permissions (unix.wat), a minimal libc (libc.c/libc.wasm), WASI memory management and errno mapping, and a Node.js wrapper for running WASI binaries under Node.
Native effects via Stack Switching (PR #2189)
OCaml 5 effect handlers need the ability to suspend a computation, run a handler, and resume. In the native OCaml compiler, this is implemented using stack segments. In wasm_of_ocaml, we rely on JSPI by default, which does not cost much when effects are not used, but is comparatively slow when effects are used as core control flow. However, there's also the option of using CPS-transformation: every function that might perform an effect gets a continuation parameter, and perform/continue/discontinue manipulate these closures explicitly. This works but has overhead -- extra closure allocations, indirect calls, and the CPS transform obscures the control flow, which inhibits other optimizations.
The WebAssembly Stack Switching proposal (currently Phase 3 in the Wasm standardization process) adds a third implementation: native primitives for creating, suspending, and resuming stacks. This maps directly onto what OCaml's effect handlers need.
This PR by Jerome Vouillon adds a --effects native flag:
wasm_of_ocaml --effects native foo.byte -o foo.js
When enabled, the CPS transformation is skipped entirely. Instead, perform suspends the current Wasm stack, the handler runs on the parent stack, and continue resumes the suspended stack -- using the primitives from the Stack Switching explainer. The implementation is in a new effect-native.wat runtime module.
This should remove the CPS overhead for code using Eio, Domain.DLS, or Lwt with its effects backend. Stack Switching is not yet in stable browser releases (Chrome/V8 has it behind a flag), so the CPS path remains the default.
Dynlink and toplevel support (PR #2187, merged 2026-04-08)
One of the most visible limitations of wasm_of_ocaml compared to js_of_ocaml was the lack of Dynlink support, which also meant no OCaml toplevel (REPL) in the browser. The OCaml Playground currently uses js_of_ocaml for exactly this reason.
This PR by Jerome Vouillon adds both. Concretely, it provides:
- A
wasm_of_ocaml_compiler_dynlinklibrary that can compile.cmofiles to Wasm and load them at runtime -- the Wasm equivalent of whatJsooTopdoes for js_of_ocaml. - A
toplevel.watruntime module implementing the low-level primitives for registering dynamically loaded code with the Wasm module system. - A
graphics.watmodule implementing the OCamlGraphicslibrary in WAT. - Virtual filesystem improvements so the toplevel can access
.cmifiles at runtime. - The Lwt toplevel example updated to build with both js_of_ocaml and wasm_of_ocaml.
- A new test suite (
compiler/tests-dynlink-wasm/) coveringloadfile,loadfile_private, compile-and-load, effects flags, and plugin dependencies.
As noted in the PR discussion, the virtual filesystem work also allows compiler/tests-toplevel to run under Wasm. This opens a path to running the OCaml.org Playground on wasm_of_ocaml instead of js_of_ocaml.
Background: Wasm_of_ocaml and OCaml-to-Wasm compilation
Wasm_of_ocaml started as a fork of js_of_ocaml by Jerome Vouillon (with major contributions from Hugo Heuzard), became a new backend in the js_of_ocaml project, and is now developed alongside it: since version 6.0.1 (the first public release, February 2025), wasm_of_ocaml and js_of_ocaml share a single repository and are released together.
It targets WasmGC (supported in Chrome 119+, Firefox 120+, and Safari 18.2+), the tail-call extension, and the exception handling extension. Using WasmGC means OCaml values are managed by the host GC directly; no custom collector is shipped, and JS interop works through shared GC'd references.
The project was presented at the ML Workshop at ICFP 2024. A separate project, Wasocaml by OCamlPro, also targets WasmGC but compiles from the Flambda IR of the native-code compiler rather than from bytecode. Jane Street has reported 2x-8x speedups from wasm_of_ocaml over js_of_ocaml on their workloads. The OCaml.org WebAssembly page has an overview of all OCaml-to-Wasm compilation options.
Getting involved
If you want to try wasm_of_ocaml, the manual covers setup with Dune. If you're already using js_of_ocaml, it's mostly a matter of adding a wasm_of_ocaml stanza:
opam install wasm_of_ocaml-compiler js_of_ocaml js_of_ocaml-ppx js_of_ocaml-lwt
wasm_of_ocaml-compiler depends on a system installation of Binaryen 119 or later.
Bug reports and contributions go to the js_of_ocaml repository. Discussion happens on the OCaml Discuss forum.
