January 23, 2022
However, my experience was generally the opposite. When using Js_of_ocaml production builds (using
So, are Js_of_ocaml generated files really that large, as the rumors suggest? I ran a small experiment to find some answers.
To run the experiment, I looked for an existing ReScript application that had some functionality that is common to most web applications:
I found a good candidate in jihchi/rescript-react-realworld-example-app. This application uses ReScript and rescript-react —the ReScript bindings to React.js— to build another example of the "mother of all demo apps". This demo app is a social blogging site (i.e. a Medium.com clone) that uses a custom API for all requests, including authentication.
To do the experiment I leveraged jsoo-react to replicate the behavior found in the original ReScript app.
jsoo-react are the bindings to React.js for Js_of_ocaml. This bindings library was originally based on
rescript-react, but over time grew apart, with
jsoo-react having more emphasis on supporting OCaml syntax.
Both bundles are generated using Webpack v4.46.0 in production mode, to avoid differences in minimization or bundling. The bundles are analyzed using webpack-bundle-analyzer.
The results shared below were created from these commits:
For each case, all components in the main
App component were commented, and then uncommented progressively, while running these commands on each step of the way:
make build-prod && yarn webpack:analyze
yarn build && yarn webpack:analyze
The results are published together with the Webpack bundle analyzer reports in https://jchavarri.github.io/jsoo-react-realworld-example-app/bundle-study/.
In any case, in the end Webpack minifier runs in both cases, so the
Gzipped columns are the meaningful ones.
|No page components||55.15 KB||51.61 KB||17.54 KB|
|+ Home||68.95 KB||65.14 KB||21.47 KB|
|+ Settings||75.45 KB||71.48 KB||23.37 KB|
|+ Login||77.86 KB||73.85 KB||23.96 KB|
|+ Register||81.01 KB||77.07 KB||24.55 KB|
|+ CreateArticle||87.09 KB||82.97 KB||26.11 KB|
|+ EditArticle||87.17 KB||83.05 KB||26.13 KB|
|+ Article||109.45 KB||104.55 KB||31.67 KB|
|+ Profile||115.52 KB||110.29 KB||33.38 KB|
|+ Favorited||115.58 KB||110.34 KB||33.39 KB|
|No page components||307.61 KB||11.16 KB||3.71 KB|
|+ Home||328.29 KB||23.10 KB||6.61 KB|
|+ Settings||349.47 KB||28.87 KB||7.9 KB|
|+ Login||359.54 KB||31.54 KB||8.27 KB|
|+ Register||372.67 KB||34.76 KB||8.53 KB|
|+ CreateArticle||391.85 KB||40.7 KB||9.46 KB|
|+ EditArticle||392.13 KB||40.79 KB||9.47 KB|
|+ Article||420.48 KB||53.96 KB||11.71 KB|
|+ Profile||435.64 KB||60.08 KB||12.69 KB|
|+ Favorited||435.91 KB||60.14 KB||12.69 KB|
It is obvious from the analysis and data above that Js_of_ocaml runtime is larger than ReScript runtime (51.61 KB vs 11.16 KB in the "No page components" case). Part of this is due to design decisions of both compilers, and there is no way around it. Js_of_ocaml needs more code as it has to provide conversion functions for types like
string, while ReScript just does not.
But I believe part of this runtime could be reduced with some careful optimizations. Js_of_ocaml runtime implementation could remove some of the runtime code if it stopped using functors for internal implementations (see more below), or gave more control to users over what functionality is available. For example, there is a "synthetic" file system available in Js_of_ocaml that gets included in bundle through the
caml_fs_init call, but this is not required for the large majority of applications.
Unlike the original ReScript application, where JSON encoders were written manually, in the Js_of_ocaml version I decided to use some tool to generate encoders and decoders to work with the JSON values that result from interacting with the public API.
After some research I decided to use ppx_jsobject_conv. This tool turned out to be a great choice as it leverages all the infrastructure from ppxlib, so it is robust and very easy to use.
However, one small thing was that it made some usage of the
Printf has a complex implementation, and it increases the bundle quite significantly. Fortunately, the usages in
ppx_jsobject_conv were quite limited and were removed without much hassle.
In general, it is recommended to avoid using
Printf functions if the bundle size budget for a Js_of_ocaml application is very limited.
This same problem is found in ReScript, which recently removed support for
Printf module altogether.
A noticeable increase can be seen in Js_of_ocaml bundle sizes when the
Article component gets added to the bundle. While ReScript app only increases by ~13KB (from 40.79 to 53.96), the Js_of_ocaml app increased by ~21KB (from 83.05 to 104.55).
Apparently, functors can not be dead code eliminated by the compiler, so all the functions that are part of the
Set module will appear in the resulting bundle, regardless if they are used or not. The good news is that the functions only appear once, so as an application grows more (and more functions from
Set are used, and more times the functor is called), the cost would remain constant.
This problem is something that ReScript has tackled by re-implementing modules like
Set in its standard library Belt in a way that they don't use functors. Maybe something similar could be done for Js_of_ocaml.
One way to keep bundle size limited is to use the browser APIs, for the cases when they are available.
Instead of ppx_yojson_conv the project uses the aforementioned ppx_jsobject_conv. The advantages of the latter is that it delegates the parse step from string to JSON to browser APIs like JSON.parse or Response.json. This removes the need of bundling additional code, and most probably leads to faster applications, as browser implementors have optimized these functions heavily over time.
One realization that might be surprising is that all applications of functions with optional labelled arguments get compiled to a bunch of comma-separated zeroes when the arguments are not passed.
This is fine for functions that take a few arguments, but in
The measurements show that Js_of_ocaml version of the application has a larger initial cost, due to the runtime being larger than that of ReScript.
Set that does not use functors.
But otherwise, for most of the incremental steps, Js_of_ocaml shows mostly the same bundle size increases than ReScript. The bundle size of both apps remain in the same order of magnitude, and it would be expected that as more components are added to the application, the difference become smaller.
We might revisit this study in the future to incorporate improvements, in which case I will add notes to this post.
I hope you enjoyed the study, if you want to share any other caveats that are missed, or there is anything inaccurate or that can be improved, feel free to reach out on Twitter.