React 实验室:三月 2023 年的工作进展
2023年3月22日 作者:Joseph Savona、Josh Story、Lauren Tan、Mengdi Chen、Samuel Susla、Sathya Gunasekaran、Sebastian Markbåge 和 Andrew 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 App Router中发布。这展示了路由器的深度集成,它真正将 RSC 作为基本组件,但这并不是构建兼容 RSC 的路由器和框架的唯一方法。RSC 规范和实现之间存在明确的功能分离。React 服务端组件旨在作为适用于兼容 React 框架的组件规范。
我们通常建议使用现有的框架,但如果你需要构建你自己的自定义框架,这是可能的。构建你自己的兼容 RSC 的框架并不像我们希望的那样容易,这主要是由于需要深度捆绑器集成。当前一代的捆绑器非常适合在客户端使用,但它们并非设计为对在服务器和客户端之间拆分单个模块图提供一流的支持。这就是为什么我们现在直接与捆绑器开发者合作,以构建内置 RSC 原语的原因。
资源加载
Suspense 允许你指定在组件的数据或代码仍在加载时在屏幕上显示的内容。这允许你的用户在页面加载以及加载更多数据和代码的路由器导航期间逐步查看更多内容。但是,从用户的角度来看,在考虑新内容是否已准备好时,数据加载和渲染并不能说明全部情况。默认情况下,浏览器会独立加载样式表、字体和图像,这可能会导致 UI 跳动和连续的布局偏移。
我们正在努力将 Suspense 与样式表、字体和图像的加载生命周期完全集成,以便 React 将它们考虑在内以确定内容是否已准备好显示。无需更改你编写 React 组件的方式,更新将以更一致和令人愉悦的方式运行。作为优化,我们还将提供一种手动方式直接从组件预加载字体等资源。
我们目前正在实施这些功能,并将很快分享更多信息。
文档元数据
应用中的不同页面和屏幕可能拥有不同的元数据,例如`<title>
`标签、描述和其他特定于此屏幕的`<meta>
`标签。从维护的角度来看,将这些信息保存在该页面或屏幕的React组件附近更具可扩展性。但是,这些元数据的HTML标签需要在文档的`<head>
`中,而`<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 转换构建的路由器(例如 Next.js App Router)尤其有用。
除了此更新外,我们的团队最近还参加了社区播客和直播,以进一步介绍我们的工作并解答问题。
- Dan Abramov 和 Joe Savona 接受了 Kent C. Dodds 在其 YouTube 频道上的采访,他们讨论了围绕 React 服务器组件的担忧。
- Dan Abramov 和 Joe Savona 做客了 JSParty 播客,并分享了他们对 React 未来发展的看法。
感谢 Andrew Clark、Dan Abramov、Dave McCabe、Luna Wei、Matt Carroll、Sean Keegan、Sebastian Silbermann、Seth Webster 和 Sophie Alpert 对这篇博文的审阅。
感谢您的阅读,我们下次更新再见!