知海

React Labs:2023 年 3 月工作进展

React博客-新闻

React Labs:2023 年 3 月工作进展

2023 年 3 月 22 日,Joseph SavonaJosh StoryLauren TanMengdi ChenSamuel SuslaSathya GunasekaranSebastian MarkbågeAndrew Clark


在 React Labs 文章中,我们分享了正在进行的研究和开发项目。自上次更新以来,我们在 React 服务器组件、资源加载、优化编译器、离屏渲染和过渡追踪方面取得了重大进展,希望与大家分享我们的心得。


React 服务器组件

React 服务器组件(React Server Components,简称 RSC)是 React 团队设计的一种新的应用架构。

我们之前在介绍性演讲RFC 中分享了有关 RSC 的研究。此前,我们引入了服务器组件这一新组件类型。服务器组件会提前运行,并在打包时被排除在客户端代码之外。服务器组件既可以在构建期间运行,让你能够读取文件系统或获取静态内容;也可以在服务器上运行,使你无需构建 API 即可访问数据层。你可以通过 props 将数据从服务器组件传递到浏览器中交互式的客户端组件。

RSC 结合了面向服务器的多页面应用程序简单直接的“请求/响应”思维模型,以及面向客户端的单页面应用程序的无缝交互性,为你带来二者兼具的体验。

自上次更新以来,我们合并了 RFC 以批准该提案。我们解决了 React 服务器模块约定 提案中悬而未决的问题,并与合作伙伴达成共识,采用 use client 约定。这些文档也构成了 RSC 兼容实现应有的规范。

最大的变化在于,我们引入了 async / await 作为在服务器组件中进行数据提取的主要方式。我们还计划通过引入一个名为 use 的新 Hook,在客户端支持数据加载,该 Hook 同样可以处理 Promise。虽然我们无法在纯客户端应用的任意组件中支持 async / await,但我们计划在客户端应用采用类似 RSC 应用的结构时,添加相应支持。

在较好地解决数据提取问题后,我们正探索另一个方向:将数据从客户端发送到服务器,以便执行数据库变更和实现表单。我们通过在服务器/客户端边界传递 Server Action 函数来实现这一点。客户端可以调用这些函数,获得无缝的 RPC 体验。此外,在 JavaScript 加载完成之前,Server Action 还能为表单提供渐进增强能力。

RSC 已在 Next.js App Router 中发布,这是一个深度集成 RSC 并将其作为原语使用的路由器示例。但这不是构建 RSC 兼容路由器和框架的唯一方式。RSC 规范和实现为特定功能提供了明确分离,旨在成为兼容 React 框架的组件规范。

我们通常建议使用现有框架,但你也可以构建自己的框架。由于需要深度集成打包器,构建自定义 RSC 兼容框架的难度比想象中更高。当前世代的打包器在客户端场景表现出色,但并非专门为将单个模块图拆分为服务器和客户端并给予一流支持而设计。因此,我们选择直接与打包器开发者合作,将 RSC 作为内置原语。

资源加载

Suspense 用于指定在组件的数据或代码仍在加载时屏幕上显示的内容。这使得页面在初始加载,或路由导航需要加载更多数据和代码时,用户能够逐步看到更多内容。然而,从用户角度看,数据和代码就绪并不完全等同于新内容已可呈现。默认情况下,浏览器会独立加载样式表、字体和图像,这可能导致 UI 跳变和持续的布局偏移。

我们正致力于将 Suspense 与样式表、字体和图像的加载生命周期完整集成,让 React 在内容就绪后再进行展示。在不改变你已编写的 React 组件方式的前提下,整个更新过程将更加连贯、令人愉悦。作为一种优化,我们也提供手动方式,可直接从组件预加载字体等资源。

这些功能目前正在实现中,很快会有更多内容分享。

Document Metadata

应用中的不同页面或屏幕往往具有不同的 metadata,例如 <title> 标签、描述和其他针对特定屏幕的 <meta> 标签。从可维护性角度看,将这些信息放在对应页面或屏幕的 React 组件附近更具可扩展性。然而,metadata 的 HTML 标签最终需要位于文档的 <head> 中,而 <head> 通常在应用根组件中渲染。

目前有两种技术方案可以解决这个问题:

一种方案是渲染一个专门的第三方组件,由其将 <title><meta> 和其他标签提升至 document <head> 中。这在主流浏览器中可行,但在不运行 JavaScript 的客户端(例如 Open Graph 解析器)中并不适用。

另一种方案是将页面拆分为两部分进行服务端渲染。首先渲染主要内容并收集其中包含的所有标签;随后渲染包含这些标签的 <head>;最后将 <head> 和主要内容一起发送给浏览器。这种方法可行,但会阻碍利用 React 18 的流式服务端渲染器,因为必须等待全部内容渲染完成后才能发送 <head>

因此,我们正在添加内置支持,让你能够在组件树中的任意位置渲染 <title><meta> 和 metadata <link> 标签。这在所有环境中都能以相同方式工作,包括纯客户端代码、SSR 以及未来的 RSC。我们很快会分享更多细节。

React 优化编译器

自上次更新以来,我们一直在积极迭代 React Forget 的设计。我们曾称之为“自动记忆化编译器”,从某种意义上说确实如此。但构建编译器让我们更深入地理解了 React 的编程模型。将 React Forget 理解为一种自动的 reactive 编译器或许是更准确的描述。

React 的核心思想是:开发者将 UI 定义为当前状态的函数。你可以使用普通 JavaScript 值(如数字、字符串、数组、对象),并通过标准 JavaScript 语法(如 if/else、for 循环等)来描述组件逻辑。其思维模型是:当应用状态发生变化时,React 会重新渲染。我们认为,这种简洁的思维模型以及紧密贴合 JavaScript 语义的原则,是 React 编程模型中的重要基础。

问题在于,React 有时会过度 reactive:它可能重新渲染太多次。例如,在 JavaScript 中,没有简单的方法比较两个对象或数组是否相等(即是否具有相同的键和值),因此在每次渲染时创建新对象或数组,可能导致 React 执行过多不必要的工作。这意味着开发者必须显式地进行记忆化,以避免组件对状态变更过度响应。

我们的目标是通过 React Forget,让 React 应用在默认情况下拥有恰到好处的 reactivity:仅当状态值发生有意义的变化时才重新渲染。从实现角度看,这需要自动记忆化;但我们认为“reactive 框架”是理解 React 与 Forget 的更好方式。可以这样理解:React 在对象标识发生变化时重新渲染;而借助 Forget,React 在语义值变化时重新渲染——且不会产生深度比较的运行时开销。

就具体进展而言,自上次更新以来,我们多次迭代了编译器设计以符合这种自动 reactive 的思路,并吸纳了内部使用编译器的反馈。在去年年底对编译器进行大规模重构后,我们已开始在 Meta 的有限生产环境中试用编译器,计划在生产环境验证后将其开源。

另外,许多人对编译器的工作原理表现出浓厚兴趣。我们期待在验证完成并开源时分享更多细节。不过现在可以先透露一部分:

编译器的核心与 Babel 基本完全解耦,其核心编译器 API(大致)接收旧 AST,输出新 AST(同时保留源码位置信息)。在底层,我们使用自定义代码表示与转换流水线(pipeline)来进行底层语义分析。但编译器的主要公共接口将经由 Babel 和其他构建系统插件提供。为了便于测试,我们目前有一个 Babel 插件,它是一个很薄的封装层,调用编译器为每个函数生成新版本并替换原版本。

过去几个月中,我们对编译器进行了重构,以聚焦于完善核心编译模型,确保能够处理条件、循环、变量重赋值和 mutation 等复杂情况。然而,JavaScript 表达每种特性都有大量不同形式:if/else、三目运算符、for、for-in、for-of 等等。如果一开始就支持整个语言,会推迟验证核心模型的时间点。因此,我们从语言的一个较小但具有代表性的子集入手:let/constif/elsefor 循环、对象、数组、原始类型、函数调用等特性。随着对核心模型信心的增强与内部抽象的完善,我们逐步扩大支持的语言子集。同时,我们也会明确记录尚不支持的语法并输出诊断信息,跳过不支持输入的编译。我们拥有相关工具,可以在 Meta 的代码库上运行编译器,以统计哪些不支持特性最常见,作为后续工作的优先依据,并逐步扩展至支持完整语言。

要让 React 组件中的原生 JavaScript 变得 reactive,需要编译器对语义有深刻理解,能准确判断代码的实际行为。通过这种方式,我们正在构建一个在 JavaScript 中实现 reactive 的系统,让开发者能够充分利用语言的全部表达能力编写任意复杂度的产品代码,而无需受限于领域特定语言。

离屏渲染

离屏渲染是 React 即将推出的一项功能,用于在不产生额外性能开销的情况下,在后台渲染屏幕。你可以把它看作 content-visibility CSS 属性 的 React 版本——不仅适用于 DOM 元素,也适用于 React 组件。在我们的研究中,离屏渲染有多种应用场景:

  • 路由可以在后台预渲染页面,当用户导航过去时立刻可用。
  • 标签页组件可以保留隐藏标签页的状态,用户切换时不会丢失进度。
  • 虚拟列表组件可以在可见窗口上下方预渲染更多行。
  • 当打开模态框或弹出层时,整个应用可以进入“后台”模式,除模态框外的全部内容禁用事件与更新。

大多数 React 开发者不会直接使用 React 的离屏 API。离屏渲染将集成到路由器和 UI 库等基础组件中,使用这些库的开发者将自动受益,无需额外配置。

我们的核心思路是:开发者应该能够不改变组件的编写方式,将任意 React 树渲染到屏幕外。当组件处于离屏渲染状态时,它实际上并未 挂载,直到组件变为可见——其 effect 不会被触发。例如,如果组件使用 useEffect 来在首次出现时记录分析数据,预渲染不会影响数据准确性。同样,当组件离开屏幕时,其 effect 也会被卸载。离屏渲染的一个关键能力是:可以切换组件的可见性而不丢失其状态。

自上次更新以来,我们已在 Meta 的 React Native 应用上测试了实验性预渲染版本,覆盖 Android 和 iOS,性能表现良好。我们还改进了离屏渲染与 Suspense 的协同方式——离屏树中的挂起(suspend)不会触发 Suspense 后备方案。剩余工作是完成向库开发者公开的基础组件。我们预计今年晚些时候发布 RFC,同时提供实验性 API 供测试和反馈。

追踪 Transition

Transition 追踪 API 可以检测 React Transition 变慢的原因并帮助定位问题。在上次更新后,我们完成了初始 API 设计,并发布了 RFC,基础功能也已实现。目前该项目处于暂停阶段。我们欢迎对 RFC 的持续反馈,并期待恢复开发,为 React 提供更好的性能测量工具。这对基于 React Transition 构建的路由来尤为有价值,例如 Next.js App Router


除本次更新外,我们团队近期还在社区播客和直播中分享了工作进展并回答了问题:

感谢 Andrew ClarkDan AbramovDave McCabeLuna WeiMatt CarrollSean KeeganSebastian SilbermannSeth WebsterSophie Alpert 对本文的审校。

感谢阅读,我们下期再见!

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。