Compiling OCaml and Haskell to Wasm
This page answers one task: you have an OCaml or Haskell codebase — a compiler, a type checker, a proof assistant, financial models — and want it running in the browser or in WASI hosts. Both languages have long compiled to JavaScript; you want to know what their WebAssembly backends offer, how to use them, and how they differ.
Prerequisites
- [ ] For OCaml: an opam switch with
wasm_of_ocaml(from the js_of_ocaml project) and a recent OCaml compiler. - [ ] For Haskell: GHC’s WebAssembly backend, typically installed via the
ghc-wasm-metatooling, targetingwasm32-wasi. - [ ] Browsers with Wasm GC support (current major browsers) for wasm_of_ocaml output.
Why functional languages are different
Languages like OCaml and Haskell allocate many small, short-lived values and rely on a fast garbage collector, tail calls, closures and (for Haskell) lazy evaluation. Compiling them to WebAssembly raises a central question: where does garbage collection happen? One option is to compile the language’s own runtime and collector into the module, managing a heap in linear memory, as C-like languages do. The other is to use the Wasm GC proposal, which lets modules allocate structs and arrays managed by the browser’s garbage collector, so the language needs no collector of its own and integrates with JavaScript objects.
The two backends here illustrate both choices. wasm_of_ocaml compiles OCaml bytecode to Wasm using Wasm GC for OCaml values, giving compact output and
cooperation with the host’s collector. GHC’s WebAssembly backend compiles Haskell to wasm32-wasi with GHC’s own runtime system (including its garbage
collector) in linear memory, and adds a JavaScript FFI for the browser.
Step 1 — compile OCaml with wasm_of_ocaml
wasm_of_ocaml works from OCaml bytecode, like js_of_ocaml, and integrates with dune:
; dune
(executable
(name main)
(modes wasm js) ; build both Wasm and JavaScript outputs
(libraries js_of_ocaml))
dune build ./main.bc.wasm.js # produces a JS loader and a .wasm (Wasm GC) module
The output is loaded from a page like any script; it requires browsers with Wasm GC and exception handling. Libraries that work with js_of_ocaml generally work with wasm_of_ocaml, including its bindings to browser APIs, which makes migration from JavaScript output largely a build change. Building both modes lets you fall back to JavaScript for older browsers.
Step 2 — call between OCaml and JavaScript
js_of_ocaml’s FFI (Js.Unsafe, typed bindings via js_of_ocaml-ppx) is used with wasm_of_ocaml too, so exposing an OCaml function to JavaScript looks the same:
let () =
Js_of_ocaml.Js.export "typecheck"
(fun (src : Js_of_ocaml.Js.js_string Js_of_ocaml.Js.t) ->
Js_of_ocaml.Js.string (Checker.run (Js_of_ocaml.Js.to_string src)))
Strings and values are converted at the boundary; keep calls coarse — pass a whole document, get a whole result — rather than calling per token.
Step 3 — compile Haskell with GHC’s Wasm backend
GHC’s WebAssembly backend is a cross-compiler. Install it with the community-maintained ghc-wasm-meta tooling, which provides wasm32-wasi-ghc and
wasm32-wasi-cabal:
wasm32-wasi-cabal build exe:checker
# produces a WASI module; run it with wasmtime, or load it in the browser with a WASI shim
wasmtime run checker.wasm < input.txt
For browser use, GHC’s JavaScript FFI lets Haskell import JavaScript functions and export Haskell functions with foreign import javascript and
foreign export javascript, and a post-link step generates the JavaScript glue. The module still uses WASI for I/O, so the page provides a WASI
implementation (a small shim library) along with the generated glue.
Step 4 — measure size and startup
Compare outputs for your code. wasm_of_ocaml output is often smaller than js_of_ocaml output and faster for compute-heavy code, because Wasm GC avoids
emulating OCaml values with JavaScript objects. GHC’s Wasm output includes the runtime system, so even small programs start at a few megabytes before
compression; it is well suited to substantial programs (compilers, interpreters, analysers) where that fixed cost is amortised. Run wasm-opt on the output
where the toolchain does not already, and serve modules compressed.
Step 5 — decide between Wasm and JavaScript output
JavaScript backends (js_of_ocaml, GHC’s JavaScript backend) remain good choices when integration with JavaScript is fine-grained (many small calls, heavy DOM work) or when you must support browsers without Wasm GC. Wasm backends shine for compute-heavy code — type checkers, solvers, simulations — and for running the same program in WASI hosts. Building both and choosing at load time, as dune makes easy for OCaml, gives the best of both during a transition.
Tail calls, exceptions and laziness
Functional code relies on tail calls; Wasm’s tail-call proposal (supported in current major browsers) lets backends compile them directly instead of using trampolines, and current toolchains use it where available. OCaml exceptions map to Wasm exception handling. Haskell’s laziness and runtime scheduling live in GHC’s RTS inside the module, so they behave as on native platforms; long-running computations still block the thread they run on, so run them in a worker.
Porting an existing codebase
Real projects depend on libraries, and those decide how hard a port is. Pure OCaml or Haskell libraries usually compile unchanged. Libraries with C stubs
need those stubs compiled to Wasm too: for wasm_of_ocaml, C stubs must be reimplemented (in JavaScript or Wasm) as they are for js_of_ocaml, so prefer pure
alternatives; for GHC’s Wasm backend, C code is compiled with the WASI SDK as part of the build, which works for portable C but not for code that depends on
POSIX features WASI lacks, such as fork or signals. Libraries that use threads, the file system or the network need care: file access maps to WASI preopens
(or a virtual file system in the browser), and networking is usually unavailable. A useful first step is to build the project’s dependency tree for the target
and list every failure; most ports are blocked by a handful of packages, which can be replaced, patched, or isolated behind an interface with a browser-specific
implementation.
Interacting with the DOM and browser APIs
Both ecosystems provide bindings to browser APIs — js_of_ocaml’s library for OCaml, and the JavaScript FFI in GHC — so a functional program can manipulate the DOM directly. In practice, many teams keep the UI in JavaScript or TypeScript and expose the functional core as a small set of exported functions: parse, check, evaluate, format. That keeps the boundary coarse, lets front-end developers work in familiar tools, and makes the compiled core reusable in Node, WASI hosts and workers without changes. Run the core in a worker for any computation that can take more than a few milliseconds.
Debugging
Compile-time names survive into both outputs to varying degrees; keep debug information in development builds, test logic natively, and use the browser’s profiler to find hot functions in the compiled module.
Expected output
An OCaml type checker built with wasm_of_ocaml loads in the browser as a 900 KB compressed module and checks a 5,000-line file 2.1× faster than its js_of_ocaml
build, with the JavaScript build as a fallback; a Haskell analyser built with the wasm32-wasi GHC runs in Wasmtime and in the browser via a WASI shim, exporting
an analyse function to JavaScript.
Gotchas
- wasm_of_ocaml in browsers without Wasm GC. The module fails to compile. Keep a JavaScript build as fallback.
- Expecting small Haskell modules. The GHC RTS adds megabytes. Use it for substantial programs.
- Fine-grained calls across the boundary. Conversion costs dominate. Pass whole inputs.
- Long computations on the main thread. Pages freeze. Run in a worker.
- Unpinned toolchains. Both backends evolve quickly. Pin versions.
Performance note
For the OCaml type checker, wasm_of_ocaml output ran the benchmark in 1.9 s versus 4.0 s for js_of_ocaml output in the same browser, with a 30% smaller compressed download.
Frequently Asked Questions
Is the GHC Wasm backend part of official GHC? Yes — it has been part of GHC since 9.6 as a cross-compilation target; tooling for installing it is maintained by the community.
Can OCaml target WASI? Projects exist for OCaml on WASI; wasm_of_ocaml focuses on browser and JavaScript hosts.
Do these support threads? OCaml 5 domains and GHC’s threaded runtime have limited or no support in these Wasm targets; plan for single-threaded execution.
What about Elm, F# or Scala? They have their own JavaScript or Wasm compilation stories; Scala.js and Kotlin/Wasm are notable examples.
What usually blocks a port? Libraries with C stubs or POSIX dependencies; build the dependency tree for the target first and replace or isolate the failing packages.
Should the UI be written in OCaml or Haskell too? It can be, but a JavaScript UI calling a small set of exported functions keeps the boundary coarse and the core reusable.
Related
- Using Wasm GC for managed languages — the GC proposal.
- Tail calls and deep recursion — tail calls.
- Running Kotlin in the browser with Kotlin/Wasm — another Wasm GC language.
- Running WASI modules in the browser — WASI shims.
← Back to Other Languages in the Browser