Web page howto

A few tips to compile and integrate your programs in web pages.


Okay! Make a minimal web page.

cat - > min.html <<EOF
<!DOCTYPE html>
<html lang="en">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <script type="text/javascript" defer="defer" src="min.js"></script>
  <title>Brr minimal example</title>
  <noscript>Sorry, you need to enable JavaScript to see this page.</noscript>

Make a minimal program.

cat - > min.ml <<EOF
open Brr
let () =
  El.set_children (Document.body G.document) El.[txt' "Hello World!"]

Compile your program to bytecode and then to JavaScript

ocamlfind ocamlc -g -linkpkg -no-check-prims -package brr min.ml
js_of_ocaml a.out -o min.js

Load the web page in your browser:

xdg-open min.html    # Linux and XDG compliant systems
open min.html        # macOS
start min.html       # Windows

And the fancy OCaml console support?

Poke your program by side effect by linking against the brr.poked library. Make sure everything gets in by using -linkall.

ocamlfind ocamlc -g -linkall -linkpkg -no-check-prims \
                 -package brr,brr.poked min.ml

Compile with js_of_ocaml. This time it needs to see the cmis and you need to add its toplevel and dynlink JavaScript support:

js_of_ocaml $(ocamlfind query -r -i-format brr.poked) -I . \
            --toplevel +toplevel.js +dynlink.js a.out -o min.js

Make sure you have the OCaml console extension installed in your browser, open min.html and click on the OCaml tab of the developer tools.

How do I run my program?

Most programs need to have the page HTML parsed to operate meaningfully. There are various ways of detecting this state but the simplest is to keep the execution of your program to a classical toplevel main invocation and integrate your script in the web page with the defer attribute:

<script type="text/javascript" defer src="myscript.js"></script>

This ensures it is fetched in parallel but only executed when the document has been parsed (visual explanation). Your main program can simply be:

let main () = Console.(log [str "DOM content loaded."])
let () = main ()

However at this point ressources like stylesheets, images, fonts, etc. may not have loaded. This means that the page layout is not accurate and you cannot compute magnitudes that depend on them. Also if you draw on a canvas with custom fonts these may not be loaded yet and be substituted by generic ones.

In these cases wait for the Brr.Ev.load event on the window to make sure all ressources are loaded. Here is a snippet that does this:

open Fut.Syntax

let main () =
  Console.(log [str "DOM content loaded."]);
  let* _ev = Ev.next Ev.load (Window.as_target G.window) in
  Console.(log [str "Resources loaded."]);
  Fut.return ()

let () = ignore (main ())

Note that your main is always non-blocking (or the browser kills you) and returns before the page loads. The callbacks you setup and the futures you trigger do however outlive that invocation.

Also, if your program, maybe indirectly, uses Stdlib.at_exit – which is not made for the browser – these functions will execute at the beginning of the life of your web page as they are automatically executed after all toplevel OCaml executions are done.

Web page size

Watch your program size. Keep the tubes unclogged. Trim convenience dependencies and try not to integrate libraries providing functionality that already exists in the browser – like JSON parsing.

js_of_ocaml performs excellent dead code elimination. All these modules of Brr you are not using won't impact your page size. However it's only as good as it can be; global mutable state may be impossible to dead code away.

Avoid mentions of Format

Unless you really want to use it, avoid any mention of Format. js_of_ocaml can't help you on that one – likely due to the presence of global mutable state in Format. With js_of_ocaml 3.6.0 and OCaml 4.09.0 this program:

open Brr
let () = Console.(log [str "Hey!"])

compiles, without special options, to 14ko. Adding this dead code:

open Brr
let pp_float = Format.pp_print_float
let () = Console.(log [str "Hey!"])

makes it 35ko, which is 2.5 larger.