Cristina Rosu:用 Incr_dom 和 OCaml 构建动态 Web 应用

原标题Cristina Rosu: Building Dynamic OCaml Web Apps with Incr_dom

从实用角度介绍 Elm 架构、虚拟 DOM 和增量计算,并现场演示计数器应用。

CRCristina Rosu讲者 · Jane Street
频道
Jane Street
发布
2018年8月24日
视频
1:14:23
阅读
11 分钟
幻灯片
9
原文
英语
在 YouTube 打开
往下滚动阅读时,视频会缩到右下角继续播放。字幕跟随阅读语言。
阅读语言共 2 种语言可读
由 AI 从英语原文翻译。打开“对照原文”可以逐段核对。
摘要

30 秒看懂

Cristina Rosu 介绍 Jane Street 的 Incr_dom 框架如何使用 OCaml 构建交互式网页界面。她现场演示增量映射如何改善大表格的更新性能,并讨论渲染、组合、测试和异步动作。

  1. 1要求严苛的表格模拟交易界面有 10,000 行频繁变化的数据,并使用部分渲染。
  2. 2模型、视图与动作Elm 架构将状态、展示和更新分开。
  3. 3差异与补丁比较虚拟 DOM 后,只需向浏览器的实际 DOM 应用一个小补丁。
  4. 4增量计算依赖图和截断机制避免重复计算未变化的结果。
  5. 530,000 行测试增量映射改善后续更新,但不能消除初次加载缓慢的问题。
  6. 6异步动作延迟执行的动作可能遇到更新后的模型,应用前应检查。

要点建议

  1. js_of_ocaml 让浏览器和服务器代码能够共享 OCaml 库与类型。
  2. Elm 架构将应用拆分为模型、视图和动作。
  3. 虚拟 DOM 减少浏览器中的改动,Incremental 则减少虚拟视图的重复计算。
  4. 增量更新无法消除构建大型初始视图的成本。
  5. 在实际应用中,部分渲染和谨慎处理异步动作仍然重要。
全文

按章节阅读整理后的全文

只看原始文稿
01

问题:大型动态网页界面

Rosu 介绍 Jane Street 的需求和一个 10,000 行的交易界面。部分渲染减少了显示的 DOM 元素数量。

在 Jane Street,我们设计了 Incr_dom,让内部网页界面更容易用 OCaml 编写。我会先解释它的主要思想,再构建一个小应用,展示这些思想如何结合。

我们的许多工程工作涉及交易系统,但也经常需要界面让用户查看和修改大量数据。我们希望能够编写网页界面,又不必让每位开发者都成为 Web 专家。

一个模拟交易系统界面说明了这个问题:它有 10,000 行,每秒变化几十次。用户可以移动焦点、编辑单元格、筛选标的并按列排序;数据变化也可能改变行的顺序。

相关数据变化时,排序和筛选必须作出响应。为提高渲染效率,示例使用部分渲染:只实际渲染当前视口中的行,用户滚动时再增加或移除行。这项技术经常与 Incr_dom 配合使用,但并非库本身的一部分。

交易表格中的部分渲染
幻灯片 1 / 9交易表格中的部分渲染开发者工具显示,大表格实际上只渲染了一小部分行。

我们的目标是让网页界面易于编写、维护和优化。我们希望开发者无需深入学习 JavaScript,就能为现有系统添加界面,而且在面对大量频繁变化的数据时,界面仍能保持响应。

02

浏览器中的 OCaml 与 Elm 架构

js_of_ocaml 让浏览器代码可以使用与后端相同的语言。Elm 模式将模型、视图和动作分开。

Incr_dom 借鉴了已有思想,尤其是 React 和 Elm。React 是用于构建交互界面的 JavaScript 库,Elm 则是专为界面设计的函数式语言;这个框架将两者的思想与 OCaml 工具结合起来。

第一块基石是 js_of_ocaml,它将 OCaml 字节码编译为 JavaScript,使 OCaml 能在浏览器中运行。这样客户端和服务器就能共享代码,后端改动更容易传播,编译错误也更容易发现。

我们还可以复用多年积累的 OCaml 库和工具,以及开发者已经掌握的语言知识。类型安全、表达能力等优点也因此适用于网页界面。

Elm 架构提供了一种组织应用的模式。它类似模型—视图—控制器:模型表示应用状态,视图展示状态,动作描述模型可能如何改变。

模型、视图与动作
幻灯片 2 / 9模型、视图与动作图示展示模型如何生成视图,以及视图如何调度动作来更新模型。

视图由模型生成。按钮点击或文本输入等事件处理器会调度动作;应用动作后产生新模型,再由此构建下一个视图。

这样的分离让每个部分都有明确职责,也让视图成为声明式的:程序员只需描述新视图应该是什么样,而不必列出修改旧界面的具体步骤。

由 Incr_dom 管理的应用需要提供模型和动作类型,以及 apply_action 和 view 函数。框架负责调度并连接各部分;apply_action 接收模型和动作,返回新模型。

应用接口
幻灯片 3 / 9应用接口模型与动作类型,以及 apply_action 和 view 函数,定义了应用的核心约定。
03

虚拟 DOM 与增量计算

虚拟 DOM 减少代价高昂的浏览器修改。增量依赖图和截断机制减少构建视图所需的工作。

最初看起来,视图生成的是 HTML DOM,即与浏览器相连的一棵可变树。修改实际 DOM 可能触发浏览器中成本高昂的工作,例如重新计算样式、布局和重绘。

每次模型变化时都重建真实 DOM 并不现实。虚拟 DOM 通过保存轻量的内存表示来解决这个问题;修改它不会直接触发浏览器工作。

在差异与补丁示例中,旧虚拟树有 A、B、C 三个节点,新树增加了 D。比较两者会产生包含这一新增内容的补丁,随后只将补丁应用到实际 DOM。

虚拟 DOM 的差异与补丁
幻灯片 4 / 9虚拟 DOM 的差异与补丁比较新旧虚拟树,为实际 DOM 生成补丁。

我们的虚拟 DOM 库提供属性和节点。节点具有标签、属性及子节点,与 HTML 非常相似;body 等便捷函数可以简化 OCaml 语法。

返回虚拟 DOM,而不是重建真实 DOM,可以减少浏览器工作,但每次模型变化后重新创建整棵虚拟树,成本仍可能过高。这时就要用到 Incremental 库。

Incremental 支持以依赖图表示的自调整计算。输入变化时,可以重新计算其后代;如果重新计算的节点值与之前相同,截断机制就会停止继续传播。

在 Incr_dom 中,Incremental 让模型到虚拟 DOM 的转换更加高效。如果模型只有一小部分变化,计算就只需更新虚拟视图的相应部分。

类型为 alpha Incr.t 的增量值表示图中的一个节点,其值的类型为 alpha。Incr.map 根据一个父节点和一个函数创建节点,map2 则组合两个父节点。

let%map 语法是一种 PPX 展开,不会改变语义。增量值位于右侧,映射函数可用的普通值位于左侧;and 用来组合多个增量输入。

举例来说,给定三个整数输入节点,可以推导出它们的最小值和最大值。再用两个节点把这些值转换为虚拟 DOM 文本,得到一张准确反映结果与输入依赖关系的图。

增量依赖图
幻灯片 5 / 9增量依赖图最小值、最大值的计算及其视图依赖三个输入。

在应用中,这些输入节点可以从增量模型中提取,再把它们的视图组合成一个 body 节点。由于每个视图节点都是模型的后代,截断机制很重要:无关模型字段变化时,未变的投影能够阻止不必要的下游重复计算。

04

增量映射

Incr_map 能高效更新经过变换的键值映射,适用于表格等重复元素。

Incr_map 变换
幻灯片 6 / 9Incr_map 变换幻灯片比较普通映射变换和增量映射变换的函数签名。

Incr_map 建立在 Incremental 之上,用于处理键值映射。普通映射变换会产生新映射;对应的增量操作接收并返回增量映射,可以根据输入差异的大小更新输出。

展示可能原本用列表表示的行或单元格时,这一点尤其有用。现在视图接口接收增量模型并生成增量虚拟 DOM,不必每次变化时都重建全部内容。

05

构建计数器应用

演示构建了包含模型、动作、事件处理器和虚拟 DOM 的应用,随后加入定时更新。

演示从一个只有标题的页面开始。Dune 文件指定 OCaml 可执行程序、依赖、js_of_ocaml 编译和预处理器;HTML 包含生成的 JavaScript,CSS 提供基本样式。

初始应用模块
幻灯片 7 / 9初始应用模块OCaml 代码为初始应用定义模型、动作、状态和视图。

OCaml 主文件实现应用接口和初始模型。起初模型与动作都是 unit,因此应用没有有意义的状态或行为。模型截断机制使用一个相等性函数,这里采用物理相等。

动作模块还指定序列化方式,以及哪些动作需要记录。单独的 State 模块保存服务器连接等命令式状态;模型则应保持不可变,以便 Incremental 按预期工作。

最初,apply_action 原样返回模型,view 创建一个简单的 body 节点。在 view 开头对整个模型做映射很容易理解,但每当模型变化,其下的所有内容都会重新计算。

应用还为其他必需的接口函数提供简单实现,并通过 Start_app.simple 启动。使用 Dune 构建并刷新浏览器后,更新后的标题确认编译出的代码正在运行。

我把 unit 模型替换成一个包含 100 个计数器的列表,初始值是 0 到 99 的随机整数。遍历列表生成表格行,每行包含索引和计数值;普通 CSS 提供边框和交替的行颜色。

计数值也可以决定自身样式:小于 10 的数显示为绿色,小于 100 的显示为蓝色,更大的数显示为红色。这些属性由普通 OCaml 代码计算,并能随数据变化而改变。

为了让表格具有交互性,我定义带索引的 Increment 和 Reset 动作变体。apply_action 对动作进行模式匹配,更新选中的计数器:Increment 加一,Reset 将值设为零。

事件处理器负责调度这些动作。视图接收 inject,它与 schedule 类似,但类型专为虚拟 DOM 事件处理器设计:单击使该行递增,双击则将其重置。

启动函数还会每秒约 20 次调度随机递增。浏览器中的数值会自行变化,单击递增,双击重置;对于 100 行,运行相当流畅。

交互式计数器表格
幻灯片 8 / 9交互式计数器表格代码和浏览器展示点击处理器、周期性递增和颜色区分的数值。
06

扩大演示规模

行数达到 30,000 时,原始应用变得迟缓。增量映射改善更新性能,增量折叠则维护总数。

为了测试规模,我让行数可以通过 URL 查询参数配置,默认值为 100。达到 30,000 行时,初次加载需要数秒,滚动变得卡顿,重置计数器也有明显延迟。

一位观众问,更换数据结构是否有帮助。我认为可能有帮助,并将列表换成映射,以便应用使用 Incr_map。不过,仅仅改变数据结构和相应调用,还不会让视图具备增量特性。

原来的视图在构建行之前会映射整个模型,因此即使只发生小改动,也会重新计算整个页面主体。我去掉这一映射,对增量模型使用 Incr.Map.mapi,并在需要普通值的地方映射得到的行。

重新编译后,第一次加载仍然很慢:Incremental 有助于更新已有计算,并不能免费完成初始工作。加载完成后,滚动以及点击或重置计数器都明显更流畅。

为了演示另一种增量映射操作,我使用 unordered_fold 添加所有计数器的总数。累加器从零开始;新增条目时加上其值,移除条目时减去其值。

计数器总数
幻灯片 9 / 9计数器总数应用在表格上方显示计算出的总数。

总数显示在表格上方,计数器变化时仍保持响应。将行数设为 10 后,重置数值更容易让人看到总数按预期下降。

07

从演示走向生产

部分渲染、可见性钩子和共享派生计算满足更实际的界面需求。Rosu 回顾了框架的设计目标。

真实应用还需要其他技术。部分渲染只渲染可见元素,并用尺寸合适的占位元素代替屏幕外内容,可以改善初始加载;尺寸正确则能避免意外跳动。

让焦点行保持在视口中需要使用 update_visibility 和 on_display。前者在 DOM 变化、窗口调整大小或滚动后调用,可以测量焦点相对视口的位置;后者可以比较当前模型和之前的模型。

这种比较让应用在用户移动焦点时将其滚动到视口中,同时避免用户有意滚动到其他位置时又被拉回去,例如用户到搜索框输入文字时。

更大的应用可能需要在 view 之外使用增量计算。在交易界面中,排序后的行既用于显示表格,也用于决定焦点如何移动;派生接口允许 view 和 apply_action 共享这一计算。

这些部分共同满足了最初的目标:js_of_ocaml 支持复用 OCaml,Elm 架构提供简单的结构,虚拟 DOM、Incremental 和部分渲染等技术则帮助大型界面保持响应。

08

问答

观众询问组件、其他框架、性能、测试和服务器通信。讨论最后涉及如何安全处理异步动作。

一位观众询问高阶组件,以及与 React、Reason 和 BuckleScript 的比较。我解释说,当时 Incr_dom 尚无明确的组件概念,虽然其模式可以组合,而且明确的组件方案正在开发。

我对这些比较对象的使用经验不足,无法给出明确评价。Incr_dom 让开发者能够控制增量性能;我听说 BuckleScript 可能落后于 OCaml 的版本,不过它生成的 JavaScript 比 js_of_ocaml 的输出更易读。

被问到 Elm 时,我说两种架构有许多共同思想,据说语言也很相似,但我对 Elm 了解不足,无法判断开发者转换起来有多容易。

一个关于大型虚拟 DOM 树的问题指出,获取映射数据的列表仍需要重建列表。我们已经注意到这一成本,目前没有针对性的解决方案;避免重建各个节点仍能节省工作量,而非常大的应用可能需要部分渲染。

另一位观众问哪些内容应放在 model,哪些应放在 state。只有模型用于生成视图,因此视图所需的全部信息都应放在模型中;模型必须不可变。服务器连接等可变资源则放在 state 中。

被问到图形和绘图时,我承认多个界面都需要它们,而现有的图形专用工具不足。构建更好的组件会很有帮助。

增量计算本身有开销,因此并非对每个小模型都一定值得使用。如果应用已经响应良好,增加增量机制可能弊大于利;需要通过实验了解其中的权衡。

关于调试和测试,我提到可以使用控制台日志,也可以考虑将虚拟 DOM 转成 HTML 进行检查。团队曾讨论改进测试工具,但尚未完全建成。

这场演讲主要关注客户端。与服务器通信时,Jane Street 使用基于 WebSocket 的 RPC;如果数据流推送更新的速度超过浏览器的处理能力,就可能出问题,因此有时轮询更合适。

对于异步请求,apply_action 会收到一个 schedule_action 函数,可用于稍后调度动作。与此同时其他动作可能运行,因此延迟动作执行时,模型可能已经变化。

这种竞态需要在应用层谨慎处理:应用异步调度的动作之前,应检查当前模型,确保该动作仍然合理。问答结束后,我感谢了观众。

同频道的其他文章

1:48:29EN → 简体中文
JSJane Street· 2026年10月16日AI 如何改变编程:John Krazit 与 Ron Minsky 的对话Ron Minsky, John Krazit · Jane StreetJane Street 的 AI 工具负责人和 Signals and Threads 主持人探讨智能体基础设施、安全、代码审查,以及软件开发体验的变化。编程智能体Developer productivity评估+2
1:36:37EN → 简体中文
JSJane Street· 2026年9月23日Aaron Bauer:在 AI 时代教授软件工程Ron Minsky, Aaron Bauer · Jane Street当模型能够生成代码时,学习目标、主动练习和编辑器设计如何影响工程教育。AI in EducationSoftware engineeringEducation+2
9 张幻灯片1:05:45EN → 简体中文
JSJane Street· 2026年9月11日Cyrus Omar:实时编程与人与 AI 共享的计算公共空间Cyrus Omar通过 Hazel 演示,探讨即时反馈、结构化协作和精细权限如何帮助人类与 AI 智能体共同工作。Programming LanguagesHuman-AI Collaboration开发流程与测试+2
1:25:00EN → 简体中文
JSJane Street· 2026年9月2日Eric Mannes:如何将杂乱的外部数据用于交易研究Ron, Eric Mannes · Jane Street从商品期货和 Excel 风险系统,到时点数据集和 AI,Eric Mannes 解释 Jane Street 数据工程背后的领域知识。Data quality & curation开发流程与测试Investing+2
7 张幻灯片1:16:29EN → 简体中文
JSJane Street· 2026年8月26日Grant Sanderson 与 Alok Puranik:从群论看位置编码Grant Sanderson, Alok Puranik · 3Blue1Brown从注意力机制的假设出发,他们推导矩阵指数,并将旋转、衰减和 Jordan 块与 RoPE、ALiBi 联系起来。Transformers & attentionMathematics机器学习基础+1
27:39EN → 简体中文
JSJane Street· 2026年8月21日3Blue1Brown 与 Jane Street 谈交易、机器学习和文化Grant Sanderson, Nitya, In Young, Craig, Alok · 3Blue1BrownGrant Sanderson 与 Jane Street 的同事讨论做市、共享算力、研究,以及他们加入公司的不同经历。Machine LearningFintechCompany Culture+2