Library / SDK
ocsigen/js_of_ocaml avatar
ocsigen/js_of_ocaml

js_of_ocaml: running OCaml bytecode in a browser without recompiling a thing

Compiler from OCaml to Javascript.

1,138 stars203 forksOCamlNOASSERTION

At a glance

What is it?
js_of_ocaml compiles OCaml bytecode to JavaScript rather than retargeting the compiler, so an existing OCaml installation and its libraries work unchanged in a browser or on Node. The trade is a different data representation, with 32-bit integers and unboxed floats, and a tail call story that is only partly optimised.
Who is it for?
js_of_ocaml suits a team that has OCaml code and libraries it wants to run on the web without a rewrite, since the bytecode interface is stable and no library needs recompiling, and the browser bindings cover a large part of the web platform. It is the wrong tool if you need OCaml's native 63-bit integers, and it is the wrong choice if you depend on the parts of the standard library the compiler does not support, most of the Sys module and the extra libraries such as Thread.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly OCaml, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A bytecode compiler, not a retargeted OCaml

js_of_ocaml is a compiler from OCaml bytecode to JavaScript, and it makes it possible to run pure OCaml programs in a JavaScript environment like browsers and Node.js.

The four advantages the README claims are worth reading together, because the third and fourth explain the design.

It is easy to install and use as it works with an existing installation of OCaml, with no need to recompile any library. That is the whole reason for the bytecode approach. A retargeted compiler would need its own standard library and would need every third-party library recompiled against it, which in practice means the OCaml ecosystem does not work with it. By taking the bytecode as input, js_of_ocaml inherits the entire library ecosystem for free.

The price is stated explicitly: we believe this compiler will prove much easier to maintain than a retargeted OCaml compiler, as the bytecode provides a very stable API. The bytecode format changes rarely, so a compiler that targets it does not have to be updated when the OCaml compiler's internals move.

The performance claim is attributed rather than asserted: the README attributes the performance claim to the project benchmarks, stating that the generated programs run typically faster than with the OCaml bytecode interpreter. Faster than the interpreter is a modest bar, and the qualifier typically matters, since the claim is about not paying an interpreter's overhead, not about matching native code.

Install and compile, two commands and a bytecode file

The install is a single opam command.

bash
opam install js_of_ocaml js_of_ocaml-compiler js_of_ocaml-ppx

Then the workflow has two steps, and the first one is the one people forget. Your program must first be compiled using the OCaml bytecode compiler ocamlc, with the JavaScript bindings provided by the js_of_ocaml package and the syntax extension provided by js_of_ocaml-ppx.

bash
ocamlfind ocamlc -package js_of_ocaml -package js_of_ocaml-ppx -linkpkg -o cubes.byte cubes.ml

Only then does the js_of_ocaml compiler produce JavaScript.

bash
js_of_ocaml cubes.byte

The -linkpkg flag is doing real work here. The ppx syntax extension rewrites OCaml expressions into the calls the JavaScript runtime expects, so the ppx package has to be linked into the bytecode, not just installed.

The examples/ directory in the repository shows the range of what the runtime can host, with boulderdash/, cubes/, graph_viewer/, graphics/, hyperbolic/, minesweeper/, planet/, webgl/, webgl2_particles/, wiki/ and wysiwyg/. The webgl and webgl2_particles directories are the interesting ones for a reader assessing the runtime's reach, since they show the WebGL bindings doing real work rather than a hello world.

The distribution is made of several packages. js_of_ocaml-compiler is the compiler, js_of_ocaml-ppx is the syntax extension, js_of_ocaml is the base library, and there are further packages for JSON derivation, lwt, tyxml, and a toplevel, plus wasm_of_ocaml-compiler, the WebAssembly counterpart described in its own README.

The data representation is where OCaml programs break

The README says data representation differs from the usual one, and that most notably integers are 32 bits rather than 31 bits or 63 bits, which is their natural size in JavaScript, and floats are not boxed.

This is the single most important thing to understand before porting, because it changes observable behaviour rather than just performance.

The consequences are listed. Marshalling floats might generate different output, and such output should not be unmarshalled using the standard OCaml runtime, native or bytecode. The polymorphic hash function will not give the same results on data structures containing floats. And these functions may be more prone to stack overflow.

The type mapping table makes the rest concrete. int, int32 and nativeint all become a 32-bit number. int64 becomes an object, MlInt64, because JavaScript has no 64-bit integer type. float becomes a number, and string and bytes become a string or an object, MlBytes. Immediate values such as true, false and None become 32-bit numbers, and blocks become arrays with the tag as the first element, so a constructor C(1,2) is [tag,1,2], a tuple is [0,1,2], and a record is [0,1,2].

So an OCaml program that relies on 63-bit integers, or that uses polymorphic hashing over floats, or that persists marshalled values, will behave differently once compiled to JavaScript. None of that is a bug in the compiler; it is the price of targeting a language without those types, and it is the thing to audit first.

What the standard library does not cover

The README states that most of the OCaml standard library is supported, then carves out exceptions.

Most of the Sys module is not supported. That is the module for command line arguments, environment variables, file system operations and process control, so an OCaml program that leans on Sys for configuration will need to move that logic into the browser bindings.

Extra libraries distributed with OCaml, such as Thread, are not supported in general. Within them, the README lists what does work: Bigarray is supported using Typed Arrays, Num is supported, Str is supported, Graphics is partially supported using canvas with a pointer to js_of_ocaml-lwt.graphics, and in Unix the time related functions are supported.

That list is a good summary of the shape of the porting effort. Numeric work ports cleanly, since Num and Bigarray are supported. Threading does not, and anything built on it will need to be rewritten on top of lwt, which has its own package. The Unix module is mostly absent, with only the time functions carried over, so a program that shells out or opens sockets has to move to the browser APIs through the bindings.

The alternative to porting is to use the browser bindings directly, which is where the js_of_ocaml base library earns its keep. The README claims it comes with bindings for a large part of the browser APIs, and the lib/ directory in the distribution is described as the library for interfacing with JavaScript APIs.

For the toplevel specifically, the README lists the extra requirements: tyxml and reactiveData, ocp-indent for indentation support, higlo for syntax highlighting, and cohttp to build the toplevel webserver.

Tail calls are only partly optimised, and effects need a flag

Two language features get partial treatment, and both are documented with enough detail to be useful.

Tail call is not optimized in general. However, mutually recursive functions are optimized. Self recursive functions, when the tail calls are the function itself, are compiled using a loop. Trampolines are used otherwise.

So the common case of a recursive loop written in OCaml style is handled well, and the more general mutual recursion falls back to a trampoline.

Effect handlers are supported with the --effects={cps,double-translation} flag. Both transformation strategies are offered, and neither is the default, which means a program using effect handlers has to opt in at compile time and choose a strategy. The double-translation strategy preserves the source-level structure at the cost of doing the work twice, and the CPS strategy avoids that. The choice is a per-program decision that the compiler cannot make for you.

For a porting project, the two checks are therefore: does the code depend on tail calls for performance or for stack depth, and does it use effect handlers at all.

The toplevel is a separate feature rather than a side effect of the compiler. The README lists supported toplevel versions, OCaml 4.04.0+BER for the MetaOCaml-based one and OCaml 4.06.0, which includes Base, Core_kernel, Async_kernel and Async_js, and the toplevel/ directory is described as a web-based OCaml toplevel.

Engines, ES5 downleveling, and the Wasm sibling

The target is stated precisely. The generated code and runtime target ECMAScript 2020. Out of the box they run on Node.js 18 or later and any evergreen web browser released since early 2020, specifically Chrome 80+, Firefox 74+, Safari 13.4+ and Edge 80+. QuickJS-NG is also supported.

For older engines, the output can be transpiled down to ES5, with a pointer to the browser compatibility section of the manual.

There is a wrinkle in the middle. The compiler optionally relies on the JavaScript WeakRef, which is part of ECMAScript 2021, to implement Stdlib.Weak and Stdlib.Ephemeron. So the ES2020 target has an ES2021 dependency behind a flag, and the ES5 path is the answer when that is not available.

The WebAssembly sibling is a separate compiler in the same repository, wasm_of_ocaml-compiler, documented in README_wasm_of_ocaml.md, and it additionally depends on a system installation of binaryen, version 119 or later. The Makefile shows the two are built and tested independently, with tests-wasm setting WASM_OF_OCAML=true for the dune build, and the CI has separate workflows for js_of_ocaml and wasm_of_ocaml.

For a project deciding between them, the constraint that decides it is the target environment, since a WebAssembly output needs a modern engine with WebAssembly support, while the JavaScript output runs on a much wider range of browsers including old mobile devices.

The engine claim has a version attached to it: the version numbers live in the VERSION file, and the constraint set is in the opam files, which the README points to for the exact version constraints.

A test suite that runs against three runtimes

The Makefile is short and shows how the project is validated.

bash
all:
	dune build @all

tests:
	dune build @runtest @runtest-js

tests-wasm:
	WASM_OF_OCAML=true dune build @runtest-wasm

The @runtest and @runtest-js aliases are the same test suite run twice, once on the native OCaml runtime and once on the JavaScript output. That is the check that matters for a compiler, because a test that passes on the bytecode but fails in JavaScript is a bug in the compiler or the runtime, and running both is how those are caught.

There is a third runtime. The tests-quickjs target runs the JS test suite using QuickJS-NG with qjs in place of node, and the engine binary can be overridden with JSOO_QUICKJS_BIN if qjs is not on PATH.

The Babel downleveling is tested too. The test-babel-downlevel target validates the recipe documented in manual/browser-compat.wiki, and it requires npm install at the repository root for @babel/cli, @babel/preset-env, core-js and es-check.

There is one interesting excluded test. The test-md5-large target is a regression test for issue 2300 covering the MD5 of an input larger than 2 GiB. It is off by default because it digests a 2 GiB sparse file, which takes about 40 seconds in JavaScript, so it is not part of make tests.

The rest of the repository supports the same discipline. There is a separate wasm_of_ocaml-compiler.opam alongside the js_of_ocaml ones, an API-STATUS.md, ECMASCRIPT.md, TESTING.md, HACKING.md, CLAUDE.md and an awesome-js_of_ocaml.md, plus a benchmarks/ directory with its own bench.Dockerfile and a Makefile target that recurses into it.

Editorial conclusion

js_of_ocaml suits a team that has OCaml code and libraries it wants to run on the web without a rewrite, since the bytecode interface is stable and no library needs recompiling, and the browser bindings cover a large part of the web platform. It is the wrong tool if you need OCaml's native 63-bit integers, and it is the wrong choice if you depend on the parts of the standard library the compiler does not support, most of the Sys module and the extra libraries such as Thread. Before you port, read the data representation section of the manual and check your code for int64, float hashing and polymorphic comparison, and run make tests, which builds the test suite for both the bytecode and the JavaScript runtimes.

Frequently asked questions

How do I compile an OCaml program to JavaScript with js_of_ocaml?

Compile it to bytecode first with ocamlfind ocamlc, linking the js_of_ocaml and js_of_ocaml-ppx packages, then run the js_of_ocaml compiler on the resulting .byte file. The ppx extension has to be linked in because it rewrites expressions into the runtime's expected calls.

What does js_of_ocaml do with OCaml integers?

Integers become 32-bit numbers, which is their natural size in JavaScript, so they are 32 bits rather than 31 or 63. The consequence is that int64 becomes an object, and polymorphic hashing and marshalling of floats can give different results than on the native runtime.

Which parts of the OCaml standard library are not supported?

Most of the Sys module is not supported, and extra libraries distributed with OCaml such as Thread are not supported in general. Within those, Bigarray, Num and Str are supported, Graphics is partially supported using canvas, and in Unix only the time related functions are supported.

Which JavaScript engines does js_of_ocaml target?

The generated code and runtime target ECMAScript 2020 and run on Node.js 18 or later and evergreen browsers released since early 2020, including Chrome 80+, Firefox 74+, Safari 13.4+ and Edge 80+. QuickJS-NG is also supported, and output can be transpiled down to ES5 for older engines.

How are the js_of_ocaml tests run?

make tests builds the test suite with dune build @runtest @runtest-js, running the same tests on the native runtime and on the JavaScript output. There are separate targets for the Wasm output and for QuickJS-NG.

Official sources

  1. Issues
  2. ocsigen/js_of_ocaml on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ocsigen-js-of-ocaml.svg)](https://hysenlabs.com/projects/ocsigen-js-of-ocaml)

Was this useful?