It’s not clear what you are measuring but I assume that’s cold builds.
Someone more versed into dune’s js_of_ocaml compilation strategy may offer better answers but the explanations must be along this:
This invokes your build in --profile=dev. With this profile dune does separate JavaScript compilation.
This entails compiling bytecode library archives (the .cma files of the libraries you use) of each of the libraries you use in your program to separate JavaScript files. You’ll pay for that only once on cold builds or when you upgrade your library dependencies.
That’s long because you also end up compiling code you won’t use (there’s a lot of stuff in brr.cma). That’s not the case if you first link your bytecode with the .cma files perform dead code elimination and then compile to JavaScript.
If you prefer the latter you should compile with --profile=release (I don’t know if it’s possible to enable or disable separate compilation in dev mode, consult the dune manual).
Use --profile=release however when your app grows the numbers are going to turn upside down for incremental builds.
Indeed, and the manual claims this is faster: JavaScript Compilation With Js_of_ocaml — Dune documentation.
However in the case of javascript (at least for small examples like this) that is clearly not true, because in dev mode I get a 2.3MB .js file, whereas in release mode a 61KB one, and producing the latter is a lot less work:
Summary
'dune build --profile=release @app' ran
9.20 ± 0.38 times faster than 'dune build --profile=dev @app'
There is a flag to force whole program mode for js_of_ocaml, but that is still slow (probably due to lack of dead code elimination, still produces a 512KB js file):
The dev profile also has other things like source maps and pretty printed output enabled, the release build doesn’t (you can see exactly what it does with --verbose).
If you add the following to your dune file it’ll speed it up considerably, however you’ll lose the source map:
Sourcemap rely on ocaml debug info, processing the additional info take some extra time, but it’s not the culprit here.
When sourcemap is enabled, as a way to preserve more locations, jsoo put each function call in its own node in the control flow graph and I believe that your artificial example is hitting some quadratic algorithm. It depends on the number of function call inside a single function.