Octane 入门:Inferno 的继任者
Octane 作为 Inferno 的继任者,将类 React 组件编译为直接 DOM 代码,并说明 hooks、依赖数组、安装方法和 beta 状态。
Octane 是由 Dominic Gannaway 开发的 JavaScript UI 框架,它接收使用 React API(useState、useEffect、memo、context、portal、Suspense)编写的组件,并提前将其编译为直接操作 DOM 的代码,因此最终交付到浏览器的产物中不存在虚拟 DOM。
如果你使用 React 已有一段时间,它的某些规则可能会让你觉得本身就是模型的一部分:hook 必须在每次渲染中以相同顺序执行、依赖数组需要手动维护、而条件式的 effect 要么被抽成一个子组件,要么在 effect 体内写一条守卫语句。这些规则大多是为了让运行时的 reconciler 正常工作,而 reconciler 恰恰是 Octane 移除的那一部分。
本文将介绍 Octane 改变了什么、哪些改变你会最先感受到、如何把它跑起来,以及这个项目目前的成熟度。
核心要点
- Octane 将 React 风格的组件编译为直接的 DOM 操作,从交付的运行时中移除了虚拟 DOM。
- hook 的身份来自它在源码中所处的位置,而不是 hook 的执行顺序,因此 hook 可以写在
if分支内部或提前 return 之后;唯一被编译器拒绝的放置位置是普通的 JavaScript 循环。 - 你可以省略依赖数组,让编译器替你读取闭包。如果你自己写了依赖数组,其含义与 React 中完全一致;传入
null则表示每次渲染都执行。 - 使用已发布的包需要 Node.js 22.22.2 或更高版本,
npm create octane my-app可用于脚手架搭建项目。 - Octane 自称为 beta 软件:运行时、编译器以及 SSR/hydration 路径都已可用,但 API 仍在变动中。
Octane 是什么,由谁开发?
Octane 将自己定位为 Inferno 的继任者:沿用你已经熟悉的 React API,同时由编译器接管目前由 React 运行时承担的三项工作,即虚拟 DOM、hook 顺序和依赖数组。Gannaway 是 Inferno 的作者,他参与过的其他项目还包括 React、Lexical、Ripple 和 Svelte。
正是这样的技术谱系,让这个项目值得你花十分钟了解,而不只是收藏起来。Inferno 的思路是把虚拟 DOM 实现做到理论上能达到的最快,从而追求性能,这也是我们此前对 Inferno.js 的分析所探讨的核心论点。Octane 保留了性能优先的目标,但把实现机制反转了过来:不是更快的 diff,而是根本不做 diff。“继任者”这一定位来自 Octane 自己的材料;Inferno 方面并没有相应的公告。
编译器思路:运行时没有虚拟 DOM
React 在每次渲染时构建一棵元素描述树,并与上一棵树进行协调(reconcile);而 Octane 则把每个模板编译成一个 DOM 节点,运行时对其进行克隆并直接打补丁。该项目的文档站点本身就是用 Octane 构建的,这是一个合理的信号,说明该编译器能够处理并非玩具级别的应用。
实际结果是,React 在运行时所做的工作(遍历树、比较 props、判断哪些发生了变化)改为在构建时决定。项目在其首页发布了一张基准测试对比表,跨多个测试套件以 Octane 为基准进行归一化。这些是项目自己测量的自有数据,页面上也没有公布硬件配置或运行日期,因此应当把它们视为有待验证的宣称,而非独立的第三方结果。
Hook 按调用位置而非调用顺序追踪
Octane 让每个 hook 的身份来自它在源码中出现的位置,而不是 hook 的执行顺序,并且它会从闭包中推断出被省略的 effect 和 memo 依赖列表。这就是为什么把 hook 写在条件语句内部是可行的。这也是日常开发中影响最大的一项改变。
以下是你在 React 中不得不写的形式,因为 hook 必须无条件执行:
function Panel({ isEditing }: Props) {
const [draft, setDraft] = useState('');
useEffect(() => {
if (!isEditing) return;
syncDraft(draft);
}, [isEditing, draft]);
if (!isEditing) return <Readonly />;
return <Editor value={draft} onChange={e => setDraft(e.target.value)} />;
}
state 和 effect 被提升到了需要它们的分支之上,而分支逻辑又在 effect 内部重复了一遍。在 Octane 中,hook 可以放在它本该在的位置:
function Panel({ isEditing }: Props) {
if (!isEditing) return <Readonly />;
const [draft, setDraft] = useState('');
useEffect(() => syncDraft(draft));
return <Editor value={draft} onInput={e => setDraft(e.currentTarget.value)} />;
}
这在实践中省掉了什么:纯粹为了让某个 hook 变成条件式而抽出的子组件、“hook 总是执行但在条件不满足时什么也不做”的写法,以及那些只为保持调用次数稳定而存在的三元表达式。注意 Octane 的事件直接来自 DOM,因此如果你希望每次按键都更新,应使用 onInput,而 onChange 会在浏览器提交编辑时触发。
项目明确提到的唯一限制是普通的 JavaScript 循环。hook 是由编译器分配的调用位置作为键的,因此 for 循环内部的按槽位标识的 hook 没有稳定身份,编译器会拒绝它。解决办法是在模板中使用带 key 的列表,或为每一项使用一个子组件。
为什么 Octane 中依赖数组是可选的?
省略依赖列表,编译器会从闭包中推断出来。自己写依赖数组,其行为与 React 中完全一致;传入 null 则表示希望每次渲染都执行。这适用于 useEffect、useMemo、useCallback 以及其他接受依赖列表的 hook。
// React:由你来维护依赖列表
useEffect(() => {
socket.subscribe(roomId, onMessage);
}, [socket, roomId, onMessage]);
// Octane:编译器读取闭包捕获了什么
useEffect(() => {
socket.subscribe(roomId, onMessage);
});
这个”逃生舱”和推断本身一样重要:显式写出的依赖数组永远不会被重写,所以任何你想要精确控制的地方,自己写出来即可。对内置 hook 的直接调用会在编译器处理的任何模块中保留这种推断能力,包括写在普通 .ts 或 .js 文件里的自定义 hook。对你自己编写的包装函数的调用则是一种更受限的情形:该包装函数必须在一个完全编译的 .tsrx 或 .tsx 模块中本地声明,并且必须把它的回调和最后一个依赖参数直接透传给一个受支持的 hook。
如何把 octanejs 跑起来?
使用已发布的包需要 Node.js 22.22.2 或更高版本。octane create 命令接受 --template spa 创建纯客户端应用,或 --template fullstack 创建包含路由、流式 SSR、hydration 和生产构建的应用;不加这个参数它会交互式询问你。
npm create octane my-app
cd my-app
npm run dev
你用哪个包管理器运行该命令,就由哪个包管理器来安装依赖,因为全新目录中没有可读取的 lockfile,这也是文档和仓库在同一步骤中展示了不同包管理器的原因。对于已有项目,快速开始指南介绍了 Vite 路线:安装 octane 和 @octanejs/vite-plugin,然后添加插件即可。该插件会一并带上编译器。Rspack 使用 @octanejs/rspack-plugin,Rsbuild 使用 @octanejs/rsbuild-plugin。
简谈 TSRX
TSRX 是编写 Octane 组件所使用的语法,承载于 .tsrx 文件中,新增了模板指令(@if、@for、@switch、@try)以及紧邻其所作用标记的作用域化 <style> 块。它本身就是一个独立的语言项目,而非 Octane 的某项特性,Octane 只是它的编译目标之一,其他还包括 React、Preact、Solid、Vue 和 Ripple。它还引入了 @{ ... },这是返回单个 JSX 元素或 fragment 的函数体的简写形式,顶部是初始化代码,最后一个节点作为输出。你并没有必须采用它的义务:TSRX vs TSX/JSX 指南指出这两种方言共享同样的 hook、context、portal、Suspense、transition、原生事件、作用域样式、服务端渲染和 hydration,其自身的建议也是:已经能正常工作的 TSX 就别动了,不要为了改而改扩展名。
Octane 目前处于什么阶段?
项目方称 Octane 为 beta 软件:运行时、编译器和 SSR/hydration 路径都已可用,但在 1.0 之前 API 仍可能变动。Octane 的更新日志显示当前发布版本处于 0.3 系列,快速开始文档中”在正式项目里锁定版本”的建议也由此而来。按项目自己的统计,核心测试套件运行了超过 3,900 项独立的行为测试,覆盖一致性、差分、hydration、运行时、编译器和 SSR 等检查。这相当于覆盖了 React 自身测试的多少比例,则是在一份自动生成的对等性报告中逐条追踪的,而不是从测试套件总数中直接读出。
互操作是双向的。ReactCompat 和 OctaneCompat 都来自 octane/react 入口:前者让真正的 React 组件能在 Octane 中运行,后者把编译后的 Octane 组件嵌入 React 应用。React 兼容性指南详细介绍了如何同时接入两个编译器、在 React 树中渲染一个 Octane 孤岛、跨边界共享 React context,以及带 hydration 的服务端渲染。
对生态要有清醒认识。Octane 提供了广泛使用的 React 库的第一方 @octanejs/* 移植版本,但各自的完成度参差不齐:有些与上游行为一致,有些被标注为部分实现或 alpha,而自动生成的 docs/bindings-status.md 表格就是你查看某个包覆盖了哪些功能、对应哪个上游版本、在哪些地方存在差异,以及是否支持 SSR 和 hydration 的地方。一组精选的第一方绑定,和 React 应用可以不假思索直接取用的整个包生态,完全是两回事。
对于”移除运行时 reconciler 之后,React 的编程模型会是什么样子”这个问题,Octane 是目前最有意思的答案,而它在开发体验上的两项改进都足够真切,一个下午就能切身感受到。先查看你所依赖项目的绑定状态表,然后搭一个用完即弃的 SPA,试着把一个 hook 写进某个分支里。
常见问题
我能否在不重写的前提下,在现有 React 应用中引入 Octane?
可以。octane/react 入口导出了 OctaneCompat,它让编译后的 Octane 子树可以栖身于真正的 React 19 树中,因此你可以一次迁移一个页面、一个小部件或一个组件。在这个孤岛内部,use() 或 useContext 可以读取其外围的 React context,事件保持原生,服务端渲染则通过从 octane/react/server 导入宿主来实现。
Context.Provider 在 Octane 中还能用吗?
不能。0.3.0 版本从 client、server 和 native context 中移除了遗留的 Context.Provider 别名,编译器现在会拒绝可静态识别的 Provider 访问,并提示你应该改写成什么。请直接把 context 本身用作 provider 组件并向其传入 value prop,或者调用 createElement(Theme, { value }, children)。render-prop 形式的 Consumer 也已移除,Octane 的 Differences from React 页面表示不会再加回来:按槽位标识的 hook 让 use() 或 useContext 可以写在条件语句内部,而这正是 Consumer 当初要解决的问题。
是否有 hook 不受 Octane『不能在循环中使用 hook』规则的限制?
有。use() 和 useContext 不占用 hook 槽位,因此在普通 JavaScript 循环中使用它们是安全的。而任何按槽位标识的 hook 在循环中都会让所有迭代共用同一个调用位置,编译器会将其报为错误。文档给出的绕行方式是使用带 key 的 @for 指令(它为每一项提供各自独立的 hook 状态),或者把 hook 下移到子组件中。
为什么 onChange 在 Octane 中的行为与 React 不同?
Octane 使用真正的委托式 DOM 事件,没有合成事件层,因此 onChange 就是浏览器原生的 change 事件:它在编辑被提交时触发,通常是失焦时,而不是每次按键。需要逐次编辑更新时请使用 onInput。受控输入仍然遵循 React 关于 value 和 checked 的规则,而 ref 是普通的 prop,而不是通过某个包装对象传递的东西。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k