Cristina Rosu: Building Dynamic OCaml Web Apps with Incr_dom

A practical introduction to the Elm architecture, virtual DOM and incremental computation, followed by a live counter-app demonstration.

CRCristina RosuIntervenant · Jane Street
Chaîne
Jane Street
Publié
24 août 2018
Vidéo
1:14:23
Lecture
9 min
Diapositives
9
Original
anglais
Ouvrir sur YouTube
En faisant défiler, la vidéo se réduit en bas à droite et continue. Les sous-titres suivent votre langue de lecture.
Langue de lectureDisponible en 2 langues
Cet article n’est pas encore disponible en français ; il s’affiche dans l’original en anglais.
RÉSUMÉ

En 30 secondes

Cristina Rosu explains how Jane Street’s Incr_dom framework uses OCaml to build interactive web interfaces. A live demonstration shows how incremental maps improve updates to a large table, while the discussion covers rendering, composition, testing and asynchronous actions.

  1. 1A demanding tableA mock trading interface has 10,000 frequently changing rows and uses partial rendering.
  2. 2Model, view and actionsThe Elm architecture separates state, presentation and updates.
  3. 3Diff and patchVirtual DOM comparison produces a small patch for the real browser DOM.
  4. 4Incremental computationDependency graphs and cutoffs avoid recomputing unchanged results.
  5. 5A 30,000-row testIncremental maps improve subsequent updates but do not eliminate slow initial loading.
  6. 6Asynchronous actionsA delayed action may encounter a newer model and should be checked before application.

À retenir

  1. js_of_ocaml lets browser and server code share OCaml libraries and types.
  2. The Elm architecture separates an application into a model, view and actions.
  3. Virtual DOM limits browser changes, while Incremental limits recomputation of the virtual view.
  4. Incremental updates do not remove the cost of constructing a large initial view.
  5. Partial rendering and careful treatment of asynchronous actions remain important in real applications.
TEXTE INTÉGRAL

Lire le texte remanié, chapitre par chapitre

Transcription seule
01

The problem: large, dynamic web interfaces

Rosu introduces Jane Street’s requirements and a trading UI with 10,000 rows. Partial rendering reduces the number of displayed DOM elements.

At Jane Street, we designed Incr_dom to make it easier to write internal web interfaces in OCaml. I will first explain its main ideas and then build a small application to show how those ideas fit together.

Much of our engineering work concerns trading systems, yet we frequently need interfaces for large amounts of data that users can inspect and change. We wanted web UIs without requiring every developer to become a web specialist.

A mock trading-system interface illustrates the problem: it has 10,000 rows, changing tens of times a second. Users can move a focus, edit cells, filter symbols and sort by columns; changing data can also change the ordering.

Sorting and filtering must respond when the relevant data changes. To make rendering efficient, the example uses partial rendering: only the rows currently in view are actually rendered, with rows added and removed as the user scrolls. This technique is commonly used with Incr_dom but is not itself part of the library.

Partial rendering in a trading table
DIAPOSITIVE 1 / 9Partial rendering in a trading tableDeveloper tools show only a small set of rows rendered for a much larger table.

Our goals were to make web UIs easy to write, maintain and optimize. We hoped a developer could add a UI to an existing system without learning JavaScript in depth, and still have it remain responsive with large, frequently changing datasets.

02

OCaml in the browser and the Elm architecture

js_of_ocaml enables browser code to share the language used on the backend. The Elm pattern separates model, view and actions.

Incr_dom draws on existing ideas, notably React and Elm. React is a JavaScript library for interactive UIs, while Elm is a functional language designed for interfaces; the framework combines ideas from them with OCaml tooling.

The first building block is js_of_ocaml, which compiles OCaml bytecode to JavaScript and makes it possible to run OCaml in a browser. This lets our client and server share code, making backend changes easier to carry through and compiler errors easier to catch.

We can also reuse the OCaml libraries and tools we have built over the years, along with the language knowledge our developers already have. Benefits such as type safety and expressiveness apply to web interfaces too.

The Elm architecture supplies a pattern for organizing the application. It resembles model-view-controller: a model represents the application state, a view displays it, and actions describe how the model may change.

Model, view and action
DIAPOSITIVE 2 / 9Model, view and actionThe diagram shows how a model generates a view, which schedules actions that update the model.

The view is produced from the model. Event handlers, such as a button click or text entry, schedule actions; applying an action produces a new model, from which the next view is constructed.

This separation gives each part a clear responsibility. It also makes the view declarative: rather than specifying a sequence of edits to the old screen, the programmer describes what the new view should look like.

An application managed by Incr_dom supplies model and action types plus apply_action and view functions. The framework supplies scheduling and connects the pieces; apply_action takes a model and an action and returns a new model.

The application interface
DIAPOSITIVE 3 / 9The application interfaceModel and action types and the apply_action and view functions define the core application contract.
03

Virtual DOM and incremental computation

Virtual DOM reduces expensive browser mutations. Incremental dependency graphs and cutoffs reduce work constructing the view.

The view initially appears to produce an HTML DOM, a mutable tree linked to the browser. Changing the actual DOM can trigger expensive browser work such as recomputing styles and layout and repainting.

Rebuilding the real DOM whenever the model changes would be impractical. Virtual DOM addresses this by keeping a lightweight in-memory representation that can be changed without directly triggering browser work.

In the diff-and-patch example, the old virtual tree has nodes A, B and C; the new tree adds D. Comparing them produces a patch containing that addition, and only the patch is applied to the actual DOM.

Virtual DOM diff and patch
DIAPOSITIVE 4 / 9Virtual DOM diff and patchOld and new virtual trees are compared to produce a patch for the real DOM.

Our virtual DOM library exposes attributes and nodes. A node has a tag, attributes and child nodes, closely mirroring HTML; convenience functions such as body make the OCaml syntax shorter.

Returning a virtual DOM instead of rebuilding the real DOM reduces browser work, but recreating the entire virtual tree after every model change can still be too costly. This is where the Incremental library comes in.

Incremental supports self-adjusting computations represented as a dependency graph. When an input changes, its descendants can be recomputed; a cutoff stops propagation when a recomputed node has the same value as before.

In Incr_dom, Incremental makes the conversion from model to virtual DOM more efficient. If only a small part of the model changes, the computation can update just the corresponding part of the virtual view.

An incremental value of type alpha Incr.t represents a graph node whose value has type alpha. Incr.map creates a node from one parent and a function, while map2 combines two parents.

The let%map syntax is a PPX expansion rather than a change in semantics. The incremental value appears on the right, and the ordinary value available to the mapped function appears on the left; and combines several incremental inputs.

For an example, take three integer input nodes and derive their minimum and maximum. Two further nodes turn those values into virtual DOM text, producing a graph that reflects exactly which results depend on which inputs.

An incremental dependency graph
DIAPOSITIVE 5 / 9An incremental dependency graphMinimum and maximum computations and their views depend on three inputs.

In an application, those input nodes can be projected from the incremental model and their views combined into a body node. Since every view node descends from the model, cutoffs matter: if an unrelated model field changes, unchanged projections stop unnecessary downstream recomputation.

04

Incremental maps

Incr_map updates a transformed key-value map efficiently. It is useful for tables and other repeated elements.

Incr_map transformation
DIAPOSITIVE 6 / 9Incr_map transformationThe slide compares ordinary and incremental map transformation signatures.

Incr_map builds on Incremental to work with key-value maps. An ordinary map transformation produces a new map; its incremental counterpart takes and returns incremental maps and can update the output in proportion to the input difference.

That is especially useful when displaying rows or cells that might otherwise be represented as lists. The view interface now takes an incremental model and produces an incremental virtual DOM rather than reconstructing everything for every change.

05

Building a counter app

The demonstration constructs an app with a model, actions, event handlers and a virtual DOM. It then introduces scheduled updates.

The demonstration starts with a page containing only a heading. The Dune file specifies the OCaml executable, dependencies, js_of_ocaml compilation and preprocessors; the HTML includes the generated JavaScript, and CSS supplies basic styling.

Starting application module
DIAPOSITIVE 7 / 9Starting application moduleThe OCaml code defines the model, action, state and view for the initial app.

The main OCaml file implements the application interface and an initial model. At first both model and action are unit, so the app has no meaningful state or behavior. The model cutoff uses an equality function, here physical equality.

The action module also specifies serialization and which actions to log. A separate State module holds imperative state such as server connections; the model should be immutable so Incremental can behave as expected.

Initially apply_action returns the model unchanged and view makes a simple body node. Mapping over the entire model at the top of view is easy to understand, but everything beneath it is recomputed whenever the model changes.

The app supplies trivial implementations of other required interface functions and starts with Start_app.simple. After building with Dune and refreshing the browser, the updated heading confirms that the compiled code is running.

I replace the unit model with a list of 100 counters, initially random integers from 0 to 99. Mapping over the list creates table rows with an index and a counter value; standard CSS provides borders and alternating row colors.

The counter values can also determine their own styles: values below 10 are green, those below 100 blue, and larger values red. These attributes are computed in ordinary OCaml and can change as the data changes.

To make the table interactive, I define Increment and Reset action variants with an index. apply_action pattern-matches on the action and updates the selected counter, adding one for Increment or replacing its value with zero for Reset.

Event handlers schedule those actions. The view receives inject, which is like schedule but has a type designed for virtual DOM event handlers: one click increments the row and a double click resets it.

The startup function also schedules random increments approximately 20 times a second. In the browser, values change on their own, clicks increment them, and double clicks reset them; for 100 rows the result is reasonably smooth.

Interactive counter table
DIAPOSITIVE 8 / 9Interactive counter tableThe code and browser show click handlers, periodic increments and color-coded values.
06

Scaling the demonstration

At 30,000 rows, the original app becomes sluggish. Incremental maps improve updates, and an incremental fold maintains a total.

To test scale, I make the row count configurable through a URL query parameter, defaulting to 100. At 30,000 rows the initial load takes several seconds, scrolling becomes choppy and resetting a counter shows visible delay.

An audience member asks whether changing the data structure might help. I agree that it could, and switch from a list to a map so the application can use Incr_map. Merely changing the data structure and corresponding calls, however, has not yet made the view incremental.

The original view mapped over the whole model before constructing rows, so even a small change recomputed the entire body. I remove that mapping and use Incr.Map.mapi on the incremental model, mapping over the resulting rows where a plain value is needed.

After recompiling, the first load remains slow: Incremental helps update an existing computation, not perform its initial work for free. Once loaded, scrolling and clicking or resetting counters are noticeably smoother.

To illustrate another incremental map operation, I add a total of all counters using unordered_fold. The accumulator starts at zero; adding an entry adds its value, and removing an entry subtracts its value.

Total over counters
DIAPOSITIVE 9 / 9Total over countersThe application displays a computed total above the table.

The total appears above the table and remains responsive as counters change. With ten rows, resetting values makes it easier to see the total decrease as expected.

07

From demonstration to production

Partial rendering, visibility hooks and shared derived computations address needs of more realistic interfaces. Rosu revisits the framework’s design goals.

A realistic application needs additional techniques. Partial rendering can improve initial load by rendering only visible elements and using an appropriately sized spacer for offscreen content; the correct size prevents unexpected jumps.

Keeping a focused row in view uses update_visibility and on_display. The former is called after DOM changes, window resizing or scrolling and can measure the focus relative to the viewport; the latter can compare the current and previous models.

That comparison lets the app scroll the focus into view when the user moves it, without dragging the user back when they have intentionally scrolled elsewhere, for example to type in a search box.

A larger app may need an incremental computation outside view. In the trading UI, sorted rows are needed both to display the table and to decide how focus moves; the derived interface permits sharing that computation between view and apply_action.

The pieces address our original goals: js_of_ocaml enables reuse of OCaml, the Elm architecture gives us a simple structure, and virtual DOM, Incremental and techniques such as partial rendering help keep large interfaces responsive.

08

Questions and answers

The audience asks about components, alternative frameworks, performance, testing and server communication. The discussion ends with handling asynchronous actions safely.

An audience member asks about higher-order components and comparisons with React, Reason and BuckleScript. I explain that at the time there is no explicit component concept in Incr_dom, although its pattern can be composed and an explicit approach is being developed.

I have not used the comparison frameworks enough to offer a definitive assessment. Incr_dom gives developers control over incremental performance; I have heard BuckleScript can lag OCaml versions, although its JavaScript output is more readable than js_of_ocaml output.

Asked about Elm, I say that the architecture shares many ideas and the languages are reportedly similar, but I do not know enough about Elm to judge how easy a developer would find the switch.

A question about large virtual DOM trees points out that obtaining the list of map data still requires reconstructing a list. We have noticed that cost and do not have a specific solution; avoiding reconstruction of individual nodes still saves work, and very large applications may need partial rendering.

Another audience member asks what belongs in model versus state. Only the model generates the view, so all information needed for that view belongs there; it must be immutable. Mutable resources such as server connections belong in state.

Asked about graphs and plots, I acknowledge that several interfaces need them and that the available graph-specific tooling is insufficient. Building better components would be useful.

Incremental computation has overhead, so it is not automatically worthwhile for every small model. If an application is already responsive, adding incremental machinery can hurt more than it helps; experimentation is needed to understand the tradeoff.

On debugging and testing, I mention console logging and the possibility of converting the virtual DOM to HTML for inspection. Testing tools were an area the team had discussed improving, but had not yet fully built.

The talk has focused on the client side. For server communication, Jane Street uses RPCs over WebSockets; a stream that pushes updates faster than the browser can process them may cause problems, so polling can sometimes be preferable.

For an asynchronous request, apply_action receives a schedule_action function that can schedule an action later. Other actions may run in the meantime, so the model may have changed by the time the deferred action is applied.

That race requires application-level care: check the current model before applying an asynchronously scheduled action to make sure it still makes sense. I thank the audience after the questions.

Sur la même chaîne

6 diapositives15:33EN
JSJane Street· 26 oct. 2026Figgie Strategy: Reading Hands, Winning Majorities and Pricing CardsBradley, Mike · Jane StreetJane Street traders Bradley and Mike analyze three games to show how private information, market competition and timing affect card prices.Decision MakingMathematicsFintech
1:48:29EN
JSJane Street· 16 oct. 2026How AI Is Changing Programming: John Krazit and Ron MinskyRon Minsky, John Krazit · Jane StreetJane Street’s AI tooling lead and the Signals and Threads host examine agent infrastructure, security, code review and the changing experience of writing software.Agents de codeDeveloper productivityÉvaluations+2
1:36:37EN
JSJane Street· 23 sept. 2026Aaron Bauer on Teaching Software Engineering in the Age of AIRon Minsky, Aaron Bauer · Jane StreetHow learning goals, active practice and editor design shape engineering education when models can generate the code.AI in EducationSoftware engineeringEducation+2
9 diapositives1:05:45EN
JSJane Street· 11 sept. 2026Cyrus Omar on Live Programming and a Computational Commons for Humans and AICyrus OmarA demonstration of Hazel shows how immediate feedback, structured collaboration and carefully scoped permissions could support people and AI agents working together.Programming LanguagesHuman-AI CollaborationWorkflows de développement et tests+2
1:25:00EN
JSJane Street· 2 sept. 2026Eric Mannes on Turning Messy External Data Into Trading ResearchRon, Eric Mannes · Jane StreetFrom commodity futures and an Excel risk system to point-in-time datasets and AI, Eric Mannes explains the domain knowledge behind data engineering at Jane Street.Data quality & curationWorkflows de développement et testsInvesting+2
7 diapositives1:16:29EN
JSJane Street· 26 août 2026Grant Sanderson and Alok Puranik: Positional Encodings Through Group TheoryGrant Sanderson, Alok Puranik · 3Blue1BrownStarting with assumptions about attention, they derive matrix exponentials and connect rotations, decay and Jordan blocks to RoPE and ALiBi.Transformers & attentionMathematicsFondamentaux du ML+1