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-meta tooling, targeting wasm32-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.

wasm_of_ocaml versus GHC's Wasm backend wasm_of_ocaml translates OCaml bytecode to Wasm using Wasm GC, so the browser's collector manages OCaml values and output stays compact, requiring browsers with Wasm GC. GHC's backend targets wasm32-wasi with GHC's runtime and garbage collector in linear memory, running in WASI hosts and in browsers through a JavaScript FFI and a WASI shim. wasm_of_ocaml (OCaml) uses Wasm GC host collector manages values needs Wasm GC in browsers compact browser output GHC Wasm backend (Haskell) wasm32-wasi target GHC RTS + GC in linear memory JS FFI for browsers full GHC semantics

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.

Building and loading a Haskell module for the browser Haskell source is compiled by the wasm32-wasi GHC cross-compiler into a WASI module that contains GHC's runtime and garbage collector. A post-link step generates JavaScript glue for foreign exports. In the browser, a WASI shim provides I/O, the glue instantiates the module, and JavaScript calls exported Haskell functions. Haskell source cabal project wasm32-wasi-ghc RTS + GC in module post-link glue foreign export javascript WASI shim in browser stdio, clocks JS calls Haskell exported functions

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.

Type-checking benchmark by OCaml backend Seconds to type-check a 5,000-line file in the browser using an OCaml type checker compiled with js_of_ocaml and with wasm_of_ocaml. seconds per check js_of_ocaml (JavaScript) 4 s wasm_of_ocaml (Wasm GC) 1.9 s

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.

← Back to Other Languages in the Browser