React 实验室:我们近期工作成果 – 2023 年 3 月

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


在 React 实验室文章中,我们会撰写有关积极研究和开发项目的文章。自我们上次更新以来,我们已经在这些项目上取得了重大进展,我们想与大家分享我们学到的东西。


React 服务器组件

React 服务器组件(或 RSC)是由 React 团队设计的一种新的应用程序架构。

我们首先在介绍性演讲RFC中分享了我们对 RSC 的研究。概括地说,我们正在引入一种新的组件——服务器组件——它可以提前运行,并且不包含在你的 JavaScript 包中。服务器组件可以在构建期间运行,让你可以从文件系统读取或获取静态内容。它们也可以在服务器上运行,让你可以访问数据层,而无需构建 API。你可以通过 props 将数据从服务器组件传递到浏览器中的交互式客户端组件。

RSC 将以服务器为中心的“请求/响应”多页面应用程序的简单思维模型与以客户端为中心的单页面应用程序的无缝交互性相结合,为你提供两全其美的体验。

自上次更新以来,我们已经合并了React 服务器组件 RFC以批准该提案。我们解决了React 服务器模块规范提案中未解决的问题,并与我们的合作伙伴达成共识,采用"use client"规范。这些文档还可以作为 RSC 兼容实现应支持内容的规范。

最大的变化是我们引入了async / await作为从服务器组件进行数据获取的主要方式。我们还计划通过引入一个名为use的新 Hook 来支持从客户端加载数据,该 Hook 用于解包 Promise。尽管我们无法在仅限客户端的应用程序中的任意组件中支持async / await,但我们计划在你将仅限客户端的应用程序构建为类似于 RSC 应用程序结构时添加对它的支持。

现在我们已经很好地解决了数据获取问题,我们正在探索另一个方向:将数据从客户端发送到服务器,以便你可以执行数据库突变并实现表单。我们正在通过让你跨服务器/客户端边界传递服务器操作函数来实现这一点,客户端可以调用这些函数,从而提供无缝的 RPC。服务器操作还可以在加载 JavaScript 之前为你提供逐步增强的表单。

React 服务器组件已在Next.js 应用路由器中发布。这展示了路由器的深度集成,它真正将 RSC 视为一个基本要素,但这并不是构建 RSC 兼容路由器和框架的唯一方法。RSC 规范和实现提供的功能之间有明确的区分。React 服务器组件旨在作为跨兼容 React 框架工作的组件的规范。

我们通常建议使用现有的框架,但如果你需要构建自己的自定义框架,也是可以的。构建自己的 RSC 兼容框架并不像我们希望的那么容易,这主要是由于需要深度集成捆绑器。当前一代的捆绑器非常适合在客户端上使用,但它们在设计时并没有考虑到对在服务器和客户端之间拆分单个模块图的一流支持。这就是我们现在直接与捆绑器开发者合作,以获取内置的 RSC 原语的原因。

资源加载

Suspense 允许你指定在组件的数据或代码仍在加载时在屏幕上显示的内容。这使得你的用户可以在页面加载期间以及在加载更多数据和代码的路由器导航期间逐步查看更多内容。然而,从用户的角度来看,在考虑新内容是否准备就绪时,数据加载和渲染并不能说明全部情况。默认情况下,浏览器会独立加载样式表、字体和图像,这会导致用户界面跳跃和连续的布局变化。

我们正在努力将 Suspense 与样式表、字体和图像的加载生命周期完全集成,以便 React 将它们考虑在内,以确定内容是否已准备好显示。无需更改你编写 React 组件的方式,更新的行为将更加一致和令人愉快。作为一种优化,我们还将提供一种手动方式来直接从组件预加载字体等资源。

我们目前正在实施这些功能,并将很快分享更多信息。

文档元数据

应用中的不同页面和屏幕可能有不同的元数据,例如特定于此屏幕的 <title> 标签、描述和其他 <meta> 标签。从维护的角度来看,将此信息保留在该页面或屏幕的 React 组件附近更具可扩展性。但是,此元数据的 HTML 标签需要位于文档 <head> 中,该标签通常在应用程序最根部的组件中呈现。

如今,人们使用以下两种技术之一来解决这个问题。

一种技术是渲染一个特殊的第三方组件,该组件将其内部的 <title><meta> 和其他标签移动到文档 <head> 中。这适用于主流浏览器,但有许多客户端不运行客户端 JavaScript,例如 Open Graph 解析器,因此这种技术并不普遍适用。

另一种技术是分两部分服务器渲染页面。首先,渲染主要内容并收集所有此类标签。然后,使用这些标签渲染 <head>。最后,将 <head> 和主要内容发送到浏览器。这种方法有效,但它会阻止您利用React 18 的流式服务器渲染器,因为您必须等待所有内容渲染完毕才能发送 <head>

这就是我们添加内置支持的原因,用于在组件树中的任何位置开箱即用地渲染 <title><meta> 和元数据 <link> 标签。它在所有环境中的工作方式都相同,包括完全客户端代码、SSR 以及未来的 RSC。我们很快就会分享更多关于这方面的细节。

React 优化编译器

自上次更新以来,我们一直在积极迭代 React Forget 的设计,这是一个针对 React 的优化编译器。我们之前将其称为“自动记忆编译器”,这在某种意义上是正确的。但构建编译器帮助我们更深入地理解了 React 的编程模型。理解 React Forget 的一个更好方法是将其视为一个自动_响应式_编译器。

React 的核心理念是开发者将 UI 定义为当前状态的函数。您可以使用普通的 JavaScript 值(数字、字符串、数组、对象)并使用标准的 JavaScript 语法(if/else、for 等)来描述您的组件逻辑。心智模型是,只要应用程序状态发生变化,React 就会重新渲染。我们相信,这种简单的心智模型和保持接近 JavaScript 语义是 React 编程模型中的一个重要原则。

问题是,React 有时可能_过于_敏感:它可能会重新渲染太多次。例如,在 JavaScript 中,我们没有廉价的方法来比较两个对象或数组是否相等(具有相同的键和值),因此在每次渲染时创建一个新对象或数组可能会导致 React 做比严格需要更多的工作。这意味着开发人员必须显式地记忆组件,以免对更改过度反应。

我们使用 React Forget 的目标是确保 React 应用程序默认情况下具有恰到好处的响应性:应用程序仅在状态值_有意义地_更改时才重新渲染。从实现的角度来看,这意味着自动记忆,但我们相信响应性框架是理解 React 和 Forget 的更好方式。一种思考方式是,React 目前在对象标识更改时重新渲染。使用 Forget,React 在语义值更改时重新渲染,但不会产生深度比较的运行时成本。

在具体进展方面,自上次更新以来,我们对编译器的设计进行了大量迭代,以符合这种自动响应性方法,并结合内部使用编译器的反馈。从去年年底开始对编译器进行了一些重大重构后,我们现在已开始在 Meta 的有限区域内在生产环境中使用编译器。我们计划在生产环境中对其进行验证后将其开源。

最后,很多人对编译器的工作原理表示感兴趣。我们期待在验证编译器并将其开源后分享更多细节。但现在我们可以分享一些信息

编译器的核心几乎完全与 Babel 解耦,并且核心编译器 API(大致)是旧 AST 输入,新 AST 输出(同时保留源代码位置数据)。在底层,我们使用自定义代码表示和转换管道来进行低级语义分析。但是,编译器的主要公共接口将通过 Babel 和其他构建系统插件。为了便于测试,我们目前有一个 Babel 插件,它是一个非常薄的包装器,它调用编译器为每个函数生成一个新版本并将其交换进来。

在过去几个月重构编译器的过程中,我们希望专注于改进核心编译模型,以确保我们能够处理诸如条件语句、循环、重新赋值和突变等复杂性。但是,JavaScript 有很多方法来表达这些特性:if/else、三元运算符、for、for-in、for-of 等。试图预先支持完整的语言会延迟我们验证核心模型的时间点。相反,我们从一个很小但具有代表性的语言子集开始:let/const、if/else、for 循环、对象、数组、基元、函数调用和一些其他特性。随着我们对核心模型的信心增强并改进内部抽象,我们扩展了支持的语言子集。我们还明确说明了我们尚不支持的语法,记录诊断信息并跳过不支持的输入的编译。我们有一些实用程序可以在 Meta 的代码库上尝试编译器,并查看哪些不支持的特性最常见,以便我们可以优先考虑这些特性。我们将继续逐步扩展以支持整个语言。

要使 React 组件中的普通 JavaScript 具有响应性,需要一个对语义有深入了解的编译器,以便它能够准确理解代码在做什么。通过采用这种方法,我们正在 JavaScript 中创建一个响应式系统,使您可以使用语言的全部表达能力编写任何复杂度的产品代码,而不局限于领域特定语言。

离屏渲染

离屏渲染是 React 中即将推出的功能,用于在后台渲染屏幕,而不会增加性能开销。您可以将其视为 content-visibility CSS 属性 的一个版本,它不仅适用于 DOM 元素,也适用于 React 组件。在我们的研究中,我们发现各种用例

  • 路由器可以在后台预渲染屏幕,以便在用户导航到这些屏幕时立即可用。
  • 选项卡切换组件可以保留隐藏选项卡的状态,以便用户可以在它们之间切换而不会丢失进度。
  • 虚拟化列表组件可以在可见窗口的上方和下方预渲染其他行。
  • 打开模态框或弹出窗口时,可以将应用程序的其余部分置于“后台”模式,以便除模态框外,所有内容的事件和更新都被禁用。

大多数 React 开发人员不会直接与 React 的离屏 API 交互。相反,离屏渲染将集成到路由器和 UI 库等内容中,然后使用这些库的开发人员将自动受益,而无需额外的工作。

我们的想法是,您应该能够在不改变组件编写方式的情况下,离屏渲染任何 React 树。当一个组件被离屏渲染时,它实际上并不会真正地*挂载*,直到该组件变得可见——它的效果不会被触发。例如,如果一个组件使用 useEffect 在它第一次出现时记录分析数据,那么预渲染不会影响这些分析数据的准确性。类似地,当一个组件离开屏幕时,它的效果也会被卸载。离屏渲染的一个关键特性是,您可以在不丢失组件状态的情况下切换组件的可见性。

自上次更新以来,我们已经在 Meta 内部测试了一个实验性的预渲染版本,用于我们在 Android 和 iOS 上的 React Native 应用中,并获得了积极的性能结果。我们还改进了离屏渲染与 Suspense 的协同工作方式——在离屏树中暂停不会触发 Suspense 回退。我们剩下的工作包括最终确定向库开发者公开的原语。我们预计将在今年晚些时候发布一份 RFC,以及用于测试和反馈的实验性 API。

过渡跟踪

过渡跟踪 API 允许您检测 React 过渡 何时变慢,并调查其变慢的原因。继上次更新之后,我们已经完成了 API 的初始设计,并发布了一份 RFC。基本功能也已实现。该项目目前处于暂停状态。我们欢迎您对 RFC 提出反馈意见,并期待着恢复其开发,以便为 React 提供更好的性能测量工具。这对于构建在 React Transitions 之上的路由器(如 Next.js App Router)尤其有用。


除了本次更新之外,我们的团队最近还作为嘉宾参加了一些社区播客和直播,就我们的工作进行了更多介绍,并回答了大家的问题。

感谢 Andrew ClarkDan AbramovDave McCabeLuna WeiMatt CarrollSean KeeganSebastian SilbermannSeth WebsterSophie Alpert 审阅了这篇文章。

感谢您的阅读,我们下次更新再见!