React 实验室:我们一直在做的工作 – 2023年3月
2023年3月22日,由Joseph Savona、Josh Story、Lauren Tan、Mengdi Chen、Samuel Susla、Sathya Gunasekaran、Sebastian Markbåge和Andrew Clark撰写
🌐 March 22, 2023 by Joseph Savona, Josh Story, Lauren Tan, Mengdi Chen, Samuel Susla, Sathya Gunasekaran, Sebastian Markbåge, and Andrew Clark
在 React Labs 的帖子中,我们会撰写关于正在进行研究和开发的项目的内容。自从我们的上次更新以来,我们在这些项目上取得了显著进展,我们想分享我们学到的东西。
🌐 In React Labs posts, we write about projects in active research and development. We’ve made significant progress on them since our last update, and we’d like to share what we learned.
React 服务器组件
🌐 React Server Components
React 服务器组件(或 RSC)是由 React 团队设计的一种新的应用架构。
🌐 React Server Components (or RSC) is a new application architecture designed by the React team.
我们首先在一场介绍性讲座和一份RFC中分享了我们关于 RSC 的研究。为了回顾它们,我们正在介绍一种新型组件——服务器组件(Server Components)——它们提前运行,并且不会包含在你的 JavaScript 包中。服务器组件可以在构建期间运行,允许你读取文件系统或获取静态内容。它们也可以在服务器上运行,让你在不必构建 API 的情况下访问数据层。你可以通过 props 将数据从服务器组件传递给浏览器中的交互式客户端组件。
🌐 We’ve first shared our research on RSC in an introductory talk and an RFC. To recap them, we are introducing a new kind of component—Server Components—that run ahead of time and are excluded from your JavaScript bundle. Server Components can run during the build, letting you read from the filesystem or fetch static content. They can also run on the server, letting you access your data layer without having to build an API. You can pass data by props from Server Components to the interactive Client Components in the browser.
RSC 将以服务器为中心的多页面应用(Multi-Page Apps)的简单“请求/响应”思维模型与以客户端为中心的单页面应用(Single-Page Apps)的无缝交互性结合在一起,为你提供两全其美的体验。
🌐 RSC combines the simple “request/response” mental model of server-centric Multi-Page Apps with the seamless interactivity of client-centric Single-Page Apps, giving you the best of both worlds.
自我们上次更新以来,我们已合并了 React 服务器组件 RFC 以批准该提案。我们解决了 React 服务器模块约定 提案中的未决问题,并与合作伙伴达成共识,采用 "use client" 约定。这些文档也作为 RSC 兼容实现应支持内容的规范。
🌐 Since our last update, we have merged the React Server Components RFC to ratify the proposal. We resolved outstanding issues with the React Server Module Conventions proposal, and reached consensus with our partners to go with the "use client" convention. These documents also act as specification for what an RSC-compatible implementation should support.
最大变化是我们引入了async / await作为从服务器组件获取数据的主要方式。我们还计划通过引入一个名为use的新 Hook 来支持从客户端加载数据,该 Hook 可以展开 Promise。虽然我们无法在纯客户端应用的任意组件中支持async / await,但当你将纯客户端应用的结构类似于 RSC 应用时,我们计划为其添加支持。
🌐 The biggest change is that we introduced async / await as the primary way to do data fetching from Server Components. We also plan to support data loading from the client by introducing a new Hook called use that unwraps Promises. Although we can’t support async / await in arbitrary components in client-only apps, we plan to add support for it when you structure your client-only app similar to how RSC apps are structured.
既然我们已经相当好地整理了数据获取,现在我们正在探索另一个方向:将数据从客户端发送到服务器,以便你可以执行数据库变更并实现表单。我们通过允许你在服务器/客户端边界上传递服务器操作函数来做到这一点,客户端然后可以调用这些函数,从而实现无缝的远程过程调用。服务器操作还可以在 JavaScript 加载之前为你提供渐进增强的表单。
🌐 Now that we have data fetching pretty well sorted, we’re exploring the other direction: sending data from the client to the server, so that you can execute database mutations and implement forms. We’re doing this by letting you pass Server Action functions across the server/client boundary, which the client can then call, providing seamless RPC. Server Actions also give you progressively enhanced forms before JavaScript loads.
React 服务器组件已在 Next.js 应用路由 中发布。这展示了路由的深度集成,它真正将 RSC 作为一个基本概念,但这并不是构建 RSC 兼容路由和框架的唯一方式。RSC 规范和实现提供的功能之间有明确的区分。React 服务器组件旨在作为一个规范,用于在兼容的 React 框架中工作的组件。
🌐 React Server Components has shipped in Next.js App Router. This showcases a deep integration of a router that really buys into RSC as a primitive, but it’s not the only way to build a RSC-compatible router and framework. There’s a clear separation for features provided by the RSC spec and implementation. React Server Components is meant as a spec for components that work across compatible React frameworks.
我们通常建议使用现有的框架,但如果你需要构建自己的自定义框架,也是可能的。构建你自己的 RSC 兼容框架并不像我们希望的那样容易,主要是因为需要深度的打包器集成。目前这一代的打包器非常适合在客户端使用,但它们在设计时并没有第一时间考虑将单个模块图同时拆分到服务器和客户端。这就是为什么我们现在直接与打包器开发者合作,以内置 RSC 的基础功能。
🌐 We generally recommend using an existing framework, but if you need to build your own custom framework, it is possible. Building your own RSC-compatible framework is not as easy as we’d like it to be, mainly due to the deep bundler integration needed. The current generation of bundlers are great for use on the client, but they weren’t designed with first-class support for splitting a single module graph between the server and the client. This is why we’re now partnering directly with bundler developers to get the primitives for RSC built-in.
资源加载
🌐 Asset Loading
Suspense 允许你指定在组件的数据或代码仍在加载时屏幕上显示的内容。这让你的用户在页面加载期间以及加载更多数据和代码的路由导航期间逐步看到更多内容。然而,从用户的角度来看,当考虑新内容是否准备好时,数据加载和渲染并不能说明全部情况。默认情况下,浏览器会独立加载样式表、字体和图片,这可能导致用户界面跳动和连续的布局变化。
我们正在努力将 Suspense 与样式表、字体和图片的加载生命周期完全整合,以便 React 将它们考虑在内,从而判断内容是否准备好显示。在不更改你编写 React 组件方式的情况下,更新将以更连贯且令人愉悦的方式表现。作为一种优化,我们还将提供一种从组件直接预加载字体等资源的手动方式。
🌐 We’re working to fully integrate Suspense with the loading lifecycle of stylesheets, fonts, and images, so that React takes them into account to determine whether the content is ready to be displayed. Without any change to the way you author your React components, updates will behave in a more coherent and pleasing manner. As an optimization, we will also provide a manual way to preload assets like fonts directly from components.
我们目前正在实现这些功能,很快会有更多内容与大家分享。
🌐 We are currently implementing these features and will have more to share soon.
文档元数据
🌐 Document Metadata
你应用中的不同页面和屏幕可能有不同的元数据,比如 <title> 标签、描述,以及针对该屏幕的其他 <meta> 标签。从维护的角度来看,将这些信息保持在该页面或屏幕的 React 组件附近更具可扩展性。然而,这些元数据的 HTML 标签需要位于文档的 <head> 中,通常在应用根组件中渲染。
🌐 Different pages and screens in your app may have different metadata like the <title> tag, description, and other <meta> tags specific to this screen. From the maintenance perspective, it’s more scalable to keep this information close to the React component for that page or screen. However, the HTML tags for this metadata need to be in the document <head> which is typically rendered in a component at the very root of your app.
如今,人们用两种技术中的一种来解决这个问题。
🌐 Today, people solve this problem with one of the two techniques.
一种技术是呈现一个特殊的第三方组件,将 <title>、<meta> 和其他标签放入其中,并将它们移动到文档 <head> 中。这种方法适用于主流浏览器,但有许多客户端不运行客户端 JavaScript,例如 Open Graph 解析器,因此这种技术并非普遍适用。
🌐 One technique is to render a special third-party component that moves <title>, <meta>, and other tags inside it into the document <head>. This works for major browsers but there are many clients which do not run client-side JavaScript, such as Open Graph parsers, and so this technique is not universally suitable.
另一种技术是将页面分为两部分进行服务器渲染。首先,渲染主要内容并收集所有此类标签。然后,用这些标签渲染 <head>。最后,将 <head> 和主要内容发送到浏览器。这种方法可行,但它会阻止你利用 React 18 的流式服务器渲染器,因为你必须等待所有内容渲染完成后才能发送 <head>。
🌐 Another technique is to server-render the page in two parts. First, the main content is rendered and all such tags are collected. Then, the <head> is rendered with these tags. Finally, the <head> and the main content are sent to the browser. This approach works, but it prevents you from taking advantage of the React 18’s Streaming Server Renderer because you’d have to wait for all content to render before sending the <head>.
这就是为什么我们要在组件树中的任何位置开箱即用地添加对渲染 <title>、<meta> 和元数据 <link> 标签的内置支持。它在所有环境中都会以相同的方式工作,包括完全客户端代码、SSR,以及将来的 RSC。我们很快会分享更多关于这方面的细节。
🌐 This is why we’re adding built-in support for rendering <title>, <meta>, and metadata <link> tags anywhere in your component tree out of the box. It would work the same way in all environments, including fully client-side code, SSR, and in the future, RSC. We will share more details about this soon.
React 优化编译器
🌐 React Optimizing Compiler
自我们上次更新以来,我们一直在积极迭代React Forget的设计,这是一个针对 React 的优化编译器。我们以前曾将其称为“自动记忆化编译器”,在某种意义上这确实如此。但构建这个编译器帮助我们对 React 的编程模型有了更深入的理解。更好的理解 React Forget 的方式是将其视为一个自动响应性编译器。
🌐 Since our previous update we’ve been actively iterating on the design of React Forget, an optimizing compiler for React. We’ve previously talked about it as an “auto-memoizing compiler”, and that is true in some sense. But building the compiler has helped us understand React’s programming model even more deeply. A better way to understand React Forget is as an automatic reactivity compiler.
React 的核心思想是,开发者将他们的用户界面定义为当前状态的一个函数。你使用普通的 JavaScript 值——数字、字符串、数组、对象——并使用标准的 JavaScript 习惯用法——if/else、for 等——来描述你的组件逻辑。其思维模型是,React 会在应用状态发生变化时重新渲染。我们认为,这种简单的思维模型以及保持接近 JavaScript 语义是 React 编程模型中的一个重要原则。
🌐 The core idea of React is that developers define their UI as a function of the current state. You work with plain JavaScript values — numbers, strings, arrays, objects — and use standard JavaScript idioms — if/else, for, etc — to describe your component logic. The mental model is that React will re-render whenever the application state changes. We believe this simple mental model and keeping close to JavaScript semantics is an important principle in React’s programming model.
问题在于 React 有时可能反应过度:它可能会重新渲染过多。例如,在 JavaScript 中,我们没有廉价的方法来比较两个对象或数组是否相等(具有相同的键和值),所以在每次渲染时创建一个新的对象或数组可能会导致 React 做比 strictly 需要更多的工作。这意味着开发者必须明确地对组件进行记忆化,以免对变化反应过度。
🌐 The catch is that React can sometimes be too reactive: it can re-render too much. For example, in JavaScript we don’t have cheap ways to compare if two objects or arrays are equivalent (having the same keys and values), so creating a new object or array on each render may cause React to do more work than it strictly needs to. This means developers have to explicitly memoize components so as to not over-react to changes.
我们使用 React Forget 的目标是确保 React 应用默认具有恰到好处的响应性:应用只有在状态值发生意义上的变化时才重新渲染。从实现的角度来看,这意味着自动进行记忆化,但我们认为把它理解为响应性框架是理解 React 和 Forget 的更好方式。可以这样理解:当前 React 在对象标识发生变化时会重新渲染。而使用 Forget,React 则在语义值发生变化时重新渲染——但不会产生深度比较的运行时开销。
🌐 Our goal with React Forget is to ensure that React apps have just the right amount of reactivity by default: that apps re-render only when state values meaningfully change. From an implementation perspective this means automatically memoizing, but we believe that the reactivity framing is a better way to understand React and Forget. One way to think about this is that React currently re-renders when object identity changes. With Forget, React re-renders when the semantic value changes — but without incurring the runtime cost of deep comparisons.
在具体进展方面,自上次更新以来,我们已经在编译器的设计上进行了大量迭代,以符合这种自动反应性的方法,并结合了内部使用编译器的反馈。自去年底开始对编译器进行了一些重大重构之后,我们现在已经在Meta的有限字段开始在生产环境中使用该编译器。一旦我们在生产中验证了它的效果,我们计划将其开源。
🌐 In terms of concrete progress, since our last update we have substantially iterated on the design of the compiler to align with this automatic reactivity approach and to incorporate feedback from using the compiler internally. After some significant refactors to the compiler starting late last year, we’ve now begun using the compiler in production in limited areas at Meta. We plan to open-source it once we’ve proved it in production.
最后,很多人对编译器的工作原理表示了兴趣。我们期待在验证编译器并开源它时分享更多细节。但现在我们可以分享一些内容:
🌐 Finally, a lot of people have expressed interest in how the compiler works. We’re looking forward to sharing a lot more details when we prove the compiler and open-source it. But there are a few bits we can share now:
编译器的核心几乎与 Babel 完全解耦,核心编译器 API(大致上)是旧 AST 输入,新 AST 输出(同时保留源代码位置信息)。在底层,我们使用自定义的代码表示和转换管道以进行低级语义分析。然而,对编译器的主要公共接口将通过 Babel 以及其他构建系统插件进行。为了便于测试,我们当前有一个 Babel 插件,它是一个非常薄的封装器,调用编译器生成每个函数的新版本并替换它。
🌐 The core of the compiler is almost completely decoupled from Babel, and the core compiler API is (roughly) old AST in, new AST out (while retaining source location data). Under the hood we use a custom code representation and transformation pipeline in order to do low-level semantic analysis. However, the primary public interface to the compiler will be via Babel and other build system plugins. For ease of testing we currently have a Babel plugin which is a very thin wrapper that calls the compiler to generate a new version of each function and swap it in.
As we refactored the compiler over the last few months, we wanted to focus on refining the core compilation model to ensure we could handle complexities such as conditionals, loops, reassignment, and mutation. However, JavaScript has a lot of ways to express each of those features: if/else, ternaries, for, for-in, for-of, etc. Trying to support the full language up-front would have delayed the point where we could validate the core model. Instead, we started with a small but representative subset of the language: let/const, if/else, for loops, objects, arrays, primitives, function calls, and a few other features. As we gained confidence in the core model and refined our internal abstractions, we expanded the supported language subset. We’re also explicit about syntax we don’t yet support, logging diagnostics and skipping compilation for unsupported input. We have utilities to try the compiler on Meta’s codebases and see what unsupported features are most common so we can prioritize those next. We’ll continue incrementally expanding towards supporting the whole language.
在 React 组件中使普通 JavaScript 具有响应性需要一个对语义有深入理解的编译器,以便它能够准确理解代码的具体作用。通过这种方法,我们正在 JavaScript 内部创建一个响应系统,这让你可以使用语言的全部表达能力编写任何复杂度的产品代码,而不必局限于特定字段的语言。
🌐 Making plain JavaScript in React components reactive requires a compiler with a deep understanding of semantics so that it can understand exactly what the code is doing. By taking this approach, we’re creating a system for reactivity within JavaScript that lets you write product code of any complexity with the full expressivity of the language, instead of being limited to a domain specific language.
离屏渲染
🌐 Offscreen Rendering
离屏渲染是在 React 中即将推出的一项功能,可在后台渲染屏幕而不会增加额外的性能开销。你可以把它看作是 content-visibility CSS 属性 的一个版本,它不仅适用于 DOM 元素,也适用于 React 组件。在我们的研究过程中,我们发现了各种使用场景:
🌐 Offscreen rendering is an upcoming capability in React for rendering screens in the background without additional performance overhead. You can think of it as a version of the content-visibility CSS property that works not only for DOM elements but React components, too. During our research, we’ve discovered a variety of use cases:
- 路由可以在后台预渲染屏幕,这样当用户导航到它们时,它们可以立即可用。
- 标签切换组件可以保留隐藏标签的状态,因此用户可以在它们之间切换而不会丢失进度。
- 虚拟列表组件可以在可见窗口的上下预渲染额外的行。
- 当打开模态窗口或弹出窗口时,其余的应用可以被置于“后台”模式,这样除了模态窗口之外的所有事件和更新都将被禁用。
大多数 React 开发者不会直接与 React 的离屏 API 交互。相反,离屏渲染将集成到诸如路由和 UI 库等功能中,然后使用这些库的开发者将自动受益,而无需额外工作。
🌐 Most React developers will not interact with React’s offscreen APIs directly. Instead, offscreen rendering will be integrated into things like routers and UI libraries, and then developers who use those libraries will automatically benefit without additional work.
这个想法是你应该能够在屏幕外渲染任何 React 树,而不改变你编写组件的方式。当一个组件在屏幕外渲染时,它实际上不会挂载,直到该组件变得可见——它的副作用不会被触发。例如,如果一个组件使用 useEffect 在第一次出现时记录分析数据,预渲染不会影响这些分析数据的准确性。同样,当一个组件移出屏幕时,它的副作用也会被卸载。屏幕外渲染的一个关键特性是,你可以在不丢失状态的情况下切换组件的可见性。
🌐 The idea is that you should be able to render any React tree offscreen without changing the way you write your components. When a component is rendered offscreen, it does not actually mount until the component becomes visible — its effects are not fired. For example, if a component uses useEffect to log analytics when it appears for the first time, prerendering won’t mess up the accuracy of those analytics. Similarly, when a component goes offscreen, its effects are unmounted, too. A key feature of offscreen rendering is that you can toggle the visibility of a component without losing its state.
自上次更新以来,我们在 Meta 内部对 React Native 应用的 Android 和 iOS 版本进行了实验性的预渲染测试,性能结果积极。我们还改进了 Suspense 的离屏渲染工作——在离屏树内挂起不会触发 Suspense 回退。我们剩下的工作包括完成向库开发者公开的原语。我们预计将在今年晚些时候发布 RFC,同时提供用于测试和反馈的实验 API。
🌐 Since our last update, we’ve tested an experimental version of prerendering internally at Meta in our React Native apps on Android and iOS, with positive performance results. We’ve also improved how offscreen rendering works with Suspense — suspending inside an offscreen tree will not trigger Suspense fallbacks. Our remaining work involves finalizing the primitives that are exposed to library developers. We expect to publish an RFC later this year, alongside an experimental API for testing and feedback.
过渡追踪
🌐 Transition Tracing
过渡追踪 API 让你能够检测 React Transitions 何时变慢,并调查可能变慢的原因。在我们上次更新之后,我们已经完成了该 API 的初步设计并发布了 RFC。基本功能也已经实现。该项目目前处于暂停状态。我们欢迎对 RFC 的反馈,并期待恢复其开发,以为 React 提供更好的性能测量工具。这对于基于 React Transitions 构建的路由,像 Next.js App Router,特别有用。
🌐 The Transition Tracing API lets you detect when React Transitions become slower and investigate why they may be slow. Following our last update, we have completed the initial design of the API and published an RFC. The basic capabilities have also been implemented. The project is currently on hold. We welcome feedback on the RFC and look forward to resuming its development to provide a better performance measurement tool for React. This will be particularly useful with routers built on top of React Transitions, like the Next.js App Router.
-
- *除了此次更新外,我们团队最近还在社区播客和直播节目中做客,讲述更多关于我们的工作并回答问题。
- Dan Abramov 和 Joe Savona 在 Kent C. Dodds 的 YouTube 通道 上接受了采访,他们讨论了围绕 React 服务器组件的担忧。
- 丹·阿布拉莫夫 和 乔·萨沃纳 是 JSParty 播客 的嘉宾,并分享了他们对 React 未来的看法。
感谢 Andrew Clark、Dan Abramov、Dave McCabe、Luna Wei、Matt Carroll、Sean Keegan、Sebastian Silbermann、Seth Webster 和 Sophie Alpert 审阅本文。
🌐 Thanks to Andrew Clark, Dan Abramov, Dave McCabe, Luna Wei, Matt Carroll, Sean Keegan, Sebastian Silbermann, Seth Webster, and Sophie Alpert for reviewing this post.
感谢阅读,我们下次更新再见!
🌐 Thanks for reading, and see you in the next update!