Understanding React Server Components
理解 React 服务器组件
React Server Components have lifted server-rendering to be a truly first-class citizen of the React ecosystem. They allow developers to render some components on the server, while attempting to abstract away the divide between the client and server. Devs can interleave Client and Server Components in their code as if all the code was running in one place.
React 服务器组件已将服务器端渲染提升为 React 生态系统中真正的一等公民。它们允许开发者在服务器上渲染某些组件,同时试图抽象掉客户端和服务器之间的界限。开发者可以在代码中交错使用客户端和服务器组件,就好像所有代码都在一个地方运行一样。
Yet, abstractions always come at a cost. What are those costs? When can you use RSCs? Does reduced bundle size mean reduced bandwidth? When should you use RSCs (React Server Components)? What are the rules that devs have to follow to use them properly and why do those rules exist?
然而,抽象总是有代价的。这些代价是什么?什么时候可以使用 RSCs?减少的包大小是否意味着减少带宽?什么时候应该使用 RSCs(React 服务器组件)?开发者必须遵循哪些规则才能正确使用它们,这些规则为什么存在?
To answer these questions, let's dive together into how React Server Components really work, under-the-hood. We'll do this by examining two sides of the RSC story: React itself and React meta-frameworks. In particular, we'll look at both React and Next.js internals to form an accurate mental model of how the RSC story comes together.
为了回答这些问题,让我们一起深入探讨 React 服务器组件(RSC)的实际工作原理,揭开其背后的奥秘。我们将通过考察 RSC 故事的两个方面来实现这一点:React 本身和 React 元框架。特别是,我们将深入研究 React 和 Next.js 的内部机制,以构建一个准确的心智模型,理解 RSC 故事是如何整合在一起的。
Note 注意
This post is aimed at developers who are familiar with using React. It assumes you know what components and hooks look like.
本文面向熟悉使用 React 的开发者,假设您了解组件和钩子的基本概念。
It's also assumed you're familiar with Promises, async, and await in JavaScript.If not, you can watch my under-the-hood YouTube video on Promises, async, and await.
还假设您熟悉 JavaScript 中的 Promises、async 和 await。如果不熟悉,您可以观看我在 YouTube 上关于 Promises、async 和 await 的底层原理视频。
For a deep dive into every aspect of React from scratch, check out my course Understanding React where we dig into React's source code to understand how JSX, Fiber, components, hooks, forms, and more really work.
要深入了解 React 的各个方面,从零开始,请查看我的课程《理解 React》,我们在课程中深入探讨 React 的源代码,以理解 JSX、Fiber、组件、钩子、表单等是如何真正工作的。
First, we must establish some fundamentals necessary to understanding how RSCs work.
首先,我们必须建立一些理解 RSC 工作原理所需的基础知识。
The DOM and Client Rendering
DOM 与客户端渲染
React co-opted the term "render". When we say the browser "renders" our page we are referring to the actual work of painting the DOM to the screen. The browser takes the DOM (the tree of elements) and the CSSOM (the tree of computed styles), calculates how elements should be laid out, and then paints the appropriate pixels to the screen.
React 借用了“渲染”这一术语。当我们说浏览器“渲染”我们的页面时,我们指的是将 DOM 绘制到屏幕上的实际工作。浏览器获取 DOM(元素树)和 CSSOM(计算样式树),计算元素的布局方式,然后将适当的像素绘制到屏幕上。
React instead uses that same term to mean "calculating what the DOM should look like". The values our component functions return tell React what the DOM should look like.
React 使用相同的术语来表示“计算 DOM 应该是什么样子”。我们的组件函数返回的值告诉 React DOM 应该是什么样子。
Thus, in the world of React (and other frameworks who have since followed in React's footsteps), when we say "client rendering" we're talking about our component functions being executed in the browser.
因此,在 React(以及其他追随 React 脚步的框架)的世界中,当我们说“客户端渲染”时,我们指的是组件函数在浏览器中执行。
React's version of "rendering" doesn't always lead to actual browser rendering, since it's possible the DOM already looks the way React thinks it should.
React 的“渲染”版本并不总是导致实际的浏览器渲染,因为 DOM 可能已经看起来像 React 认为它应该的样子。
When React says "client rendering" we're talking about our component functions being executed in the browser.
当 React 提到“客户端渲染”时,我们指的是组件函数在浏览器中执行。
In fact, a major point of React's core architecture (along with all other JS frameworks) is to limit how much its internal code updates the DOM.
事实上,React 核心架构(以及所有其他 JS 框架)的一个主要重点是限制其内部代码更新 DOM 的频率。
Tree Reconciliation 树形调和
Inside React's source code are calls to browser DOM APIs like appendChild
to update the DOM in the client. React chooses when to execute those browser DOM APIs via tree reconciliation and diffing.
在 React 的源代码中,存在对浏览器 DOM API(如 appendChild
)的调用,用于在客户端更新 DOM。React 通过树协调和差异算法来决定何时执行这些浏览器 DOM API。
As we talked about in my post on React Compiler, React keeps track of what the DOM both currently looks like and should look like in a tree of JavaScript objects, where each node is called a Fiber.
正如我在 React 编译器文章中提到的,React 通过一个 JavaScript 对象树来追踪 DOM 当前的样子以及它应该呈现的样子,其中每个节点被称为 Fiber。
React calculates what the DOM should look like (called "Work-In-Progress") and compares it to what the DOM currently looks like (called "Current") in two branches of that JavaScript object tree.
React 计算 DOM 应该是什么样子(称为“进行中的工作”),并将其与 DOM 当前的样子(称为“当前”)在该 JavaScript 对象树的两个分支中进行比较。
It then reconciles the difference between those two trees, calculating the steps needed to convert the current tree into the work-in-progress tree. Those steps are the "diff" or "patch".
然后,它会调和这两棵树之间的差异,计算出将当前树转换为进行中的树所需的步骤。这些步骤就是“差异”或“补丁”。
Once it finishes that calculation, all against simple JavaScript objects, it knows what steps to take in the real DOM. By finding the minimum number of steps to take, it minimizes how much it has to update the DOM, since updating the DOM is expensive and causes the browser to re-render (layout elements and paint pixels).
一旦完成这些计算,所有操作都是针对简单的 JavaScript 对象进行的,它就知道在真实的 DOM 中需要采取哪些步骤。通过找到需要采取的最少步骤,它最大限度地减少了必须更新 DOM 的次数,因为更新 DOM 是昂贵的操作,会导致浏览器重新渲染(布局元素和绘制像素)。
Updating the DOM in the client has the advantage of preserving state as you update the UI. For example, a user can type information into a form, React can update the UI based on some event, and the text stays in the form (unlike if the page refreshed).
在客户端更新 DOM 的优势在于,可以在更新 UI 时保留状态。例如,用户可以在表单中输入信息,React 可以根据某些事件更新 UI,而文本仍保留在表单中(与页面刷新不同)。
Thus React is focused on updating the DOM in the client, while trying to be as efficient as it can in doing so, doing work first against a fake DOM. This fake copy of the DOM's structure in JavaScript is generally called the "Virtual DOM".
因此,React 专注于在客户端更新 DOM,同时尽可能高效地完成这一任务,首先针对一个虚拟 DOM 进行操作。这个用 JavaScript 表示的 DOM 结构副本通常被称为“虚拟 DOM”。
Is "Virtual DOM" the Right Phrase?
“虚拟 DOM”这个说法准确吗?
We used to call the collection of JavaScript objects in React that are structured like the DOM the "Virtual DOM". But React doesn't like that term now because you can target other things like native iOS and Android apps (React Native) to render to.
我们过去将 React 中结构类似于 DOM 的 JavaScript 对象集合称为“虚拟 DOM”。但现在 React 不喜欢这个术语,因为你可以针对其他目标,如原生 iOS 和 Android 应用(React Native)进行渲染。
In fact, there are multiple trees that React deals with, a tree of React Elements (JS objects) that your function components return and a Fiber tree (also JS objects) that React Elements are converted into and used to store state among other things.
事实上,React 处理的是多棵树结构:由函数组件返回的 React 元素树(JS 对象),以及 React 元素被转换并用于存储状态等的 Fiber 树(同样是 JS 对象)。
While I usually refer to these two trees more specifically, for this post I'll use the long-standing colloquial term "Virtual DOM" because it's useful in keeping track of how RSCs work.
虽然我通常会更具体地称呼这两棵树,但在这篇文章中,我将使用长期以来的俗称“虚拟 DOM”,因为它有助于跟踪 RSC 的工作原理。
Calculating the Virtual DOM is what React refers to as "rendering": executing your function components to determine what the real DOM should look like. This all happens inside the JavaScript engine executing the functions, and until reconciliation doesn't touch the real DOM at all.
计算虚拟 DOM 是 React 所称的“渲染”:执行你的函数组件以确定真实 DOM 应该是什么样子。这一切都发生在执行函数的 JavaScript 引擎内部,在协调之前完全不会触及真实 DOM。
As it turns out, understanding that React co-opted the term "render" goes a surprisingly long way in helping explain RSCs accurately.
事实证明,理解 React 借用“渲染”这一术语对于准确解释 RSCs 有着出乎意料的帮助。
To understand RSCs we need to have clear in our minds the difference between what we usually mean in web dev by client and server rendering, and React's focus on the generation of the Virtual DOM.
要理解 RSCs,我们需要清楚地区分通常在网络开发中所说的客户端和服务器端渲染,以及 React 对生成虚拟 DOM 的关注点。
When React says "render", you don't actually necessarily see anything happen...
当 React 说“渲染”时,你实际上并不一定会看到任何变化...
Let's keep track of these various "render" meanings as we go. We'll call the typical (non-React) definitions "classical" and start building a dictionary entry for our web dev vocabulary:
让我们在继续的过程中跟踪这些不同的“渲染”含义。我们将典型的(非 React)定义称为“经典”,并开始为我们的 Web 开发词汇表构建一个条目:
- rend·er 渲染
- /ˈrendər/
- verb 动词
-
1. (Classical Client-Side) To take the DOM and CSSOM, compute layout, and paint pixels to the screen.
(经典客户端)获取 DOM 和 CSSOM,计算布局,并将像素绘制到屏幕上。 -
2. (React Client-Side) To execute function components in order to build and update the Virtual DOM.
(React 客户端)为了构建和更新虚拟 DOM 而执行函数组件。
The DOM and Server Rendering
DOM 与服务器端渲染
Moving forward in the RSC story means moving backwards in time. A core idea of the internet has long been HTML being delivered from a server.
在 RSC 故事中前进意味着在时间上倒退。互联网的一个核心理念长期以来一直是服务器交付 HTML。
Creating your HTML on the server (perhaps using server technologies like NodeJS or PHP) is classically called "server-side rendering" or "server rendering". This is already different than what we meant by classical client rendering. Historically server rendering has meant "generate strings of HTML" on the server.
在服务器上创建 HTML(可能使用 NodeJS 或 PHP 等服务器技术)传统上被称为“服务器端渲染”或“服务器渲染”。这已经与我们所说的传统客户端渲染有所不同。历史上,服务器渲染意味着在服务器上“生成 HTML 字符串”。
This comes with some advantages. Browsers translate HTML into the DOM very quickly. As a result HTML renders in the browser fast. Updating the DOM via JavaScript is slower in comparison. Also, your server is closer to your database or file storage, so those operations are more efficient.
这带来了一些优势。浏览器能够非常快速地将 HTML 转换为 DOM。因此,HTML 在浏览器中的渲染速度很快。相比之下,通过 JavaScript 更新 DOM 则较慢。此外,您的服务器更接近数据库或文件存储,因此这些操作更加高效。
A downside is that, while the client can request the HTML again, you lose state (the page refreshes).
一个缺点是,虽然客户端可以再次请求 HTML,但你会丢失状态(页面会刷新)。
This has been the balancing act for many years in web development: server-rendered HTML appears quickly, but DOM updates via client-side JavaScript let you make changes while maintaining the state of the page.
多年来,这始终是网页开发中的一项平衡术:服务器渲染的 HTML 能迅速呈现,而通过客户端 JavaScript 进行的 DOM 更新则允许你在保持页面状态的同时做出更改。
React doing both is nothing new. While React does DOM updates via client-side JavaScript, developers have long been able to server-render React components (SSR) as well.
React 同时执行这两项任务并不新鲜。虽然 React 通过客户端 JavaScript 进行 DOM 更新,但开发者长期以来也能够对 React 组件进行服务器端渲染(SSR)。
The server (running its own JavaScript engine via something like NodeJS) executes the components and generates an HTML string to send to the client, but there's a big caveat: all the JavaScript code for those same components also had to be sent to the client and executed.
服务器(通过类似 NodeJS 的方式运行其自己的 JavaScript 引擎)执行组件并生成 HTML 字符串发送给客户端,但有一个重要的注意事项:这些相同组件的所有 JavaScript 代码也必须发送给客户端并执行。
Why? So the Virtual DOM could be built from what those function components return. The Virtual DOM is used to "hydrate" the real DOM, meaning for example we know what click event inside what function component to run when a button is clicked. Remember, React needs both trees (DOM and Virtual DOM) to exist in the client to work. So, SSR in React means executing your functions twice (once on the server to make HTML and once on the client to make the Virtual DOM).
为什么?因为虚拟 DOM 可以从这些函数组件返回的内容中构建。虚拟 DOM 用于“水合”真实 DOM,这意味着例如我们知道当按钮被点击时,要运行哪个函数组件内部的点击事件。记住,React 需要两棵树(DOM 和虚拟 DOM)在客户端存在才能工作。因此,React 中的 SSR 意味着你的函数会被执行两次(一次在服务器上生成 HTML,一次在客户端生成虚拟 DOM)。
Here's a visualization of the SSR/hydration process to help:
以下是 SSR/水合过程的可视化,以帮助理解:
Enter React Server Components. RSCs add the ability to intermingle React components that execute on the server with React components that execute on the client without sending and re-executing the server components' JavaScript code. This also comes with the possibility of initially rendering HTML on the server, before beginning to update the DOM in the browser.
引入 React 服务器组件(RSCs)。RSCs 使得能够在服务器上执行的 React 组件与在客户端上执行的 React 组件混合使用,而无需发送并重新执行服务器组件的 JavaScript 代码。这还带来了在浏览器开始更新 DOM 之前,先在服务器上渲染 HTML 的可能性。
How? 如何?
First, let's update our dictionary entry:
首先,让我们更新字典条目:
- rend·er 渲染
- /ˈrendər/
- verb 动词
-
1. (Classical Client-Side) To take the DOM and CSSOM, compute layout, and paint pixels to the screen.
(经典客户端)获取 DOM 和 CSSOM,计算布局,并将像素绘制到屏幕上。 -
2. (React Client-Side) To execute function components in order to build and update the Virtual DOM.
(React 客户端)为了构建和更新虚拟 DOM 而执行函数组件。 -
3. (Classical Server-Side) To generate HTML to be sent to the client to build the DOM.
(经典服务器端)生成 HTML 发送给客户端以构建 DOM。 -
4. (React Server-Side SSR) To execute function components in order to generate HTML to be sent to the client to build the DOM.
(React 服务器端 SSR)执行函数组件以生成 HTML,发送到客户端以构建 DOM。
What About Server-Side Generation?
服务器端生成呢?
One thing we aren't talking about here is SSG (Server-Side Generation). That means pre-generating the HTML while building the app (that is, preparing it for deployment). You can do this for both Client and Server Components.
我们这里没有讨论的是 SSG(服务器端生成)。这意味着在构建应用程序时预先生成 HTML(即为部署做准备)。你可以对客户端和服务器组件都这样做。
SSG would have the same definition as SSR in our dictionary entry. Differentiating SSG and SSR doesn't help us much in this post, so I won't talk about it much, but it is supported.
SSG 在我们的词典条目中与 SSR 具有相同的定义。在这篇文章中区分 SSG 和 SSR 对我们帮助不大,所以我不会过多讨论它,但它是受支持的。
We said React needs both full trees, DOM and Virtual DOM, to be sitting in the browser's memory to work. So how do React Server Components get away with only executing on the server, without needing their JavaScript code to be downloaded and executed by the client?
我们提到 React 需要完整的树结构,包括 DOM 和虚拟 DOM,都驻留在浏览器的内存中才能工作。那么,React 服务器组件是如何做到仅在服务器端执行,而无需客户端下载并执行其 JavaScript 代码的呢?
React needs both full trees, DOM and Virtual DOM, to be sitting in the browser's memory to work.
React 需要完整的树结构,包括 DOM 和虚拟 DOM,都驻留在浏览器的内存中才能正常工作。
In other words, how does React build the Virtual DOM in the browser for the part defined by functions executed on the server?
换句话说,React 是如何在浏览器中为服务器上执行的函数定义的部分构建虚拟 DOM 的?
Flight 航班
In order to support executing function components on the server, and then building the Virtual DOM from their results on the client, React added the ability to serialize the React Element tree returned from server-executed functions.
为了支持在服务器上执行函数组件,然后在客户端根据其结果构建虚拟 DOM,React 增加了序列化从服务器执行函数返回的 React 元素树的能力。
Serialization and deserialization often end up meaning "convert objects in a computer's memory into a string" and "convert strings back into objects in a computer's memory".
序列化和反序列化通常意味着“将计算机内存中的对象转换为字符串”以及“将字符串转换回计算机内存中的对象”。
In this case, the results of our component functions need to be serialized and sent to the client.
在这种情况下,我们的组件函数的结果需要被序列化并发送到客户端。
Let's suppose I'm making a simple app where I will track how many students have enrolled in my React course. I'll start with a basic RSC in Next.js. It will be executed on the server.
假设我正在开发一个简单的应用程序,用于跟踪有多少学生报名参加了我的 React 课程。我将从 Next.js 中的一个基本 RSC(React Server Component)开始。它将在服务器上执行。
export default function Home() {
return (
<main>
<h1>understandingreact.com</h1>
</main>
);
}
Isomorphic Components 同构组件
If a component can be executed on the server or the client it's referred to as "isomorphic". My above function doesn't do anything server-specific (like connect directly to a database or read a file off the server), so it could instead have been executed on the client, and React could build the Virtual DOM from its results directly as normal.
如果一个组件可以在服务器或客户端上执行,它被称为“同构的”。我上面的函数没有做任何特定于服务器的事情(比如直接连接到数据库或从服务器读取文件),所以它本可以在客户端上执行,React 可以像往常一样直接从其结果构建虚拟 DOM。
If a function is isomorphic, then it can be shared. Both Server and Client Components can import and use them.
如果一个函数是同构的,那么它可以被共享。服务器组件和客户端组件都可以导入并使用它们。
To prevent having to send this function to the client for execution, its results need to be serialized. Inside React's codebase this serialization format is called "flight" and the sum of data sent is called the "RSC Payload".
为了防止必须将此函数发送到客户端执行,其结果需要进行序列化。在 React 的代码库中,这种序列化格式被称为“flight”,而发送的数据总和被称为“RSC Payload”。
My simple function's result ends up serialized into this:
我的简单函数的结果最终被序列化为这样:
"[\"$\",\"main\",null,{\"children\":[\"$\",\"h1\",null,{\"children\":\"understandingreact.com\"},\"$c\"]},\"$c\"]"
Let's format it for easier examination (credit for the excellent RSC parser from Alvar Lagerlöf):
让我们格式化它以方便检查(感谢 Alvar Lagerlöf 提供的优秀 RSC 解析器):
{
"type": "main",
"key": null,
"props": {
"children": {
"type": "h1",
"key": null,
"props": {
"children": "understandingreact.com"
}
}
Can you see the structure of the Virtual DOM? Our main
and h1
elements are here as well as our plain text node. We can see what is being passed as props, specifically the standard "children" prop intrinsic to React.
你能看到虚拟 DOM 的结构吗?我们的 main
和 h1
元素以及纯文本节点都在这里。我们可以看到作为 props 传递的内容,特别是 React 固有的标准“children”属性。
We're simplifying here, there's more to the format than this, and a meta-framework may add more to it for their own purposes. For example, identifiers for what kind of thing is being placed in the tree "like 'f:' for 'flight'". But a simplified example is sufficient for our understanding.
我们在这里进行了简化,格式的内容远不止这些,元框架可能会为了自身目的添加更多内容。例如,标识树中放置的是哪种类型的东西,比如用“f:”表示“航班”。但一个简化的例子足以帮助我们理解。
While React is providing the serialization format, the meta-framework (in this case Next.js) must do the work of ensuring the payload is created and sent to the client.
虽然 React 提供了序列化格式,但元框架(在此情况下是 Next.js)必须负责确保创建有效负载并将其发送到客户端。
Next.js, for example, has a function in its codebase called generateDynamicRSCPayload
.
例如,Next.js 在其代码库中有一个名为 generateDynamicRSCPayload
的函数。
The meta-framework is ensuring that the payload is generated and sent to the client. Thanks to the payload, on the client React can build an accurate Virtual DOM and do its normal reconciliation work.
元框架确保生成有效载荷并将其发送到客户端。得益于有效载荷,React 可以在客户端构建准确的虚拟 DOM 并执行其正常的协调工作。
Meta-Frameworks and Server Rendering
元框架与服务器渲染
We said earlier that rendering HTML from RSCs was a "possibility". That's because it's optional - it's up to the meta-framework if it does so or not. But it makes sense to do so.
我们之前提到,从 RSCs 渲染 HTML 是一种“可能性”。这是因为它是可选的——是否这样做取决于元框架。但这样做是有意义的。
Remember we said perceived performance was an important metric. If you're already executing code on the server, and you can stream back HTML, you should, because the browser will render that HTML quickly, resulting in a faster perceived experience for the user.
记得我们说过感知性能是一个重要指标。如果你已经在服务器上执行代码,并且可以流式传输回 HTML,那么你应该这样做,因为浏览器会快速渲染该 HTML,从而为用户带来更快的感知体验。
If a meta-framework is perceived as slow, no one will use it. So React meta-frameworks implementing RSCs need to do both kinds of server rendering: the classical kind and the React kind.
如果元框架被认为速度慢,那么没有人会使用它。因此,实现 RSC 的 React 元框架需要同时进行两种服务器渲染:传统类型和 React 类型。
Classical server rendering (generating HTML) gets you pages that render (painted by the browser) quickly, while React-style server rendering (RSC payload) gets you the Virtual DOM for future stateful updates.
传统的服务器渲染(生成 HTML)能让页面快速呈现(由浏览器绘制),而 React 风格的服务器渲染(RSC 负载)则为你提供了用于未来状态更新的虚拟 DOM。
Thus, in practice, an RSC will result in what is called the "double data problem". You will send the same information from the server in two different formats at the same time: HTML and Payload. You're sending the info needed to immediately build the DOM (HTML) and the Virtual DOM (Payload).
因此,在实践中,RSC 会导致所谓的“双数据问题”。您将以两种不同的格式同时从服务器发送相同的信息:HTML 和 Payload。您发送的是立即构建 DOM(HTML)和虚拟 DOM(Payload)所需的信息。
Here's a visualization: 这是一个可视化示例:
For our simple example, Next.js returns HTML, which the browser uses to build the DOM:
对于我们简单的示例,Next.js 返回 HTML,浏览器使用它来构建 DOM:
<main>
<h1>understandingreact.com</h1>
</main>
and the Payload, which React uses to build the Virtual DOM:
以及 Payload,React 使用它来构建虚拟 DOM:
{
"type": "main",
"key": null,
"props": {
"children": {
"type": "h1",
"key": null,
"props": {
"children": "understandingreact.com"
}
}
The HTML being sent allows the page to be browser rendered quickly. The user sees something right away. The Payload being sent lets React finish the work of making the page interactive.
发送的 HTML 使得页面能够快速在浏览器中渲染。用户能够立即看到内容。发送的有效载荷让 React 完成使页面具有交互性的工作。
Sometimes it's argued that the cost of this repetition of data is negated by compression algorithms (like gzip) which servers use before sending responses. However, HTML and the JSON-ish payload are two different formats, so the repetition is obfuscated enough that the double data still makes an noticeable impact on bandwidth.
有时人们认为,服务器在发送响应前使用的压缩算法(如 gzip)可以抵消这种数据重复的成本。然而,HTML 和类似 JSON 的负载是两种不同的格式,因此重复部分被混淆得足够多,以至于双倍数据仍然对带宽产生显著影响。
Abstractions have a cost. The cost here is sending the same information twice.
抽象是有代价的。这里的代价是发送了两次相同的信息。
With all this in mind, though, we now have a complete list of 5 different definitions for "render"!
综上所述,我们现在有了“render”的 5 种不同定义的完整列表!
- rend·er 渲染
- /ˈrendər/
- verb 动词
-
1. (Classical Client-Side) To take the DOM and CSSOM, compute layout, and paint pixels to the screen.
(经典客户端)获取 DOM 和 CSSOM,计算布局,并将像素绘制到屏幕上。 -
2. (React Client-Side) To execute function components in order to build and update the Virtual DOM.
(React 客户端)为了构建和更新虚拟 DOM 而执行函数组件。 -
3. (Classical Server-Side) To generate HTML to be sent to the client to build the DOM.
(经典服务器端)生成 HTML 发送给客户端以构建 DOM。 -
4. (React Server-Side SSR) To execute function components in order to generate HTML to be sent to the client to build the DOM.
(React 服务器端 SSR)执行函数组件以生成 HTML,发送给客户端以构建 DOM。 -
5. (React Server-Side RSC) To execute function components in order to generate flight (payload) data to be sent to the client to build and update the Virtual DOM.
(React 服务器端 RSC)执行函数组件以生成飞行(payload)数据,发送给客户端以构建和更新虚拟 DOM。
Notice similarities across the React definitions? For React rendering always means "executing function components", and Client and Server Components both provide what is needed to build and update the Virtual DOM.
注意到 React 定义中的相似之处了吗?对于 React 来说,渲染始终意味着“执行函数组件”,而客户端组件和服务器组件都提供了构建和更新虚拟 DOM 所需的内容。
Streams, Suspense, and RSCs
流、Suspense 和 RSCs
Performance is always a concern when building an application. But there are two kinds of performance: actual performance and perceived performance.
在构建应用程序时,性能始终是一个关注点。但性能有两种:实际性能和感知性能。
HTTP and browsers have long supported streaming as a way to improve both kinds of performance. Things like NodeJS' Stream API and the browser's Streams API (in particular the browser's ReadableStream object).
HTTP 和浏览器长期以来都支持流式传输,以提高两种性能。例如 NodeJS 的 Stream API 和浏览器的 Streams API(特别是浏览器的 ReadableStream 对象)。
React (and any meta-framework that wants to support RSCs) utilize these core technologies to stream both HTML and Payload data. Streaming really means sending small amounts (called chunks) at a time. The client can work with those smaller amounts of data as they come in.
React(以及任何希望支持 RSC 的元框架)利用这些核心技术来流式传输 HTML 和 Payload 数据。流式传输实际上意味着一次发送少量数据(称为块)。客户端可以在这些较小的数据块到达时进行处理。
Thus with streams the question isn't "what was sent" but "what has been sent over time".
因此,对于流来说,问题不在于“发送了什么”,而在于“随着时间的推移发送了什么”。
The browser is designed to handle HTML streaming over the network. It renders (lays out and paints) the page as HTML streams in.
浏览器设计用于处理通过网络传输的 HTML 流。它会在 HTML 流进入时渲染(布局和绘制)页面。
Similarly, React accepts a Promise which later resolves to RSC Payload data. Next.js, for example, sets up a ReadableStream on the client, reads in the stream from the server, and gives it to React as it comes in. React's entire approach to server rendering is centered around streaming content in where needed.
同样地,React 接受一个稍后解析为 RSC 有效负载数据的 Promise。例如,Next.js 在客户端设置一个 ReadableStream,从服务器读取流,并在数据到达时将其提供给 React。React 的整个服务器渲染方法都围绕着在需要的地方流式传输内容展开。
In fact, the Flight format itself includes markers for things that haven't completed yet. Like Promises and lazy loading.
事实上,Flight 格式本身包含了尚未完成内容的标记,比如 Promises 和懒加载。
For example, suppose I setup a server component to be async, and await a timer:
例如,假设我将服务器组件设置为异步,并等待一个计时器:
// components/Delayed.js
async function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export default async function DelayedMessage() {
await delay(5000); // 2 second delay
return (
<p>This message was loaded after a 5 second delay!</p>
);
}
// page.js
import DelayedMessage from "./components/DelayedMessage";
export default function Home() {
return (
<main>
<h1>understandingreact.com</h1>
<DelayedMessage />
</main>
);
}
The async function returns a Promise. Thus the resulting Payload will look like this:
异步函数返回一个 Promise。因此,生成的 Payload 将如下所示:
{
"type": "main",
"key": null,
"props": {
"children": [
{
"type": "h1",
"key": null,
"props": {
"children": "understandingreact.com"
}
},
{
"$$type": "reference",
"id": "d",
"identifier": "L",
"type": "Lazy node"
}
]
}
}
Notice that the place where the DelayedMessage
component should be is instead marked with a special "L" identifier, marking a place where content will appear later.
请注意,原本应放置 DelayedMessage
组件的位置被一个特殊的“L”标识符所标记,表示此处稍后将出现内容。
If you run this code, though you'll find that instead the entire page takes 5 seconds to load, rather than just the delayed message.
如果你运行这段代码,你会发现整个页面需要 5 秒钟才能加载完成,而不仅仅是延迟的消息。
That's because React deals with Promises and lazy loading using the special Suspense
functionality designed for the client. If we update our component to use Suspense
:
这是因为 React 使用专门为客户端设计的特殊 Suspense
功能来处理 Promise 和懒加载。如果我们将组件更新为使用 Suspense
:
import DelayedMessage from "./components/DelayedMessage";
import { Suspense } from "react";
export default function Home() {
return (
<main>
<h1>understandingreact.com</h1>
<Suspense fallback={<p>Loading...</p>}>
<DelayedMessage />
</Suspense>
</main>
);
}
and we run the page, it will instead show the fallback first and then show the delayed message after 5 seconds. But notice this component still runs on the server! How can you opt into Suspense
on the server? You don't. The Payload returned from the function, when processed on the client, builds a Virtual DOM that includes a
当我们运行页面时,它会先显示回退内容,然后在 5 秒后显示延迟消息。但请注意,这个组件仍然在服务器上运行!如何在服务器上选择 Suspense
呢?你不需要。当函数返回的 Payload 在客户端处理时,会构建一个包含边界的虚拟 DOM。
The Payload looks like this:
Payload 看起来像这样:
{
"type": "main",
"key": null,
"props": {
"children": [
{
"type": "h1",
"key": null,
"props": {
"children": "understandingreact.com"
}
},
{
"type": {
"$$type": "reference",
"id": "d",
"identifier": "",
"type": "Reference"
},
"key": null,
"props": {
"fallback": {
"type": "p",
"key": null,
"props": {
"children": "Loading..."
}
},
"children": {
"$$type": "reference",
"id": "e",
"identifier": "L",
"type": "Lazy node"
}
}
}
}
}
Notice both the fallback (as a prop) and what is loaded after the Promises resolves (the "Lazy node", in this case the DelayedMessage
) are all there.
注意到回退(作为属性)和 Promise 解析后加载的内容(“懒加载节点”,在此例中为 DelayedMessage
)都存在于其中。
The Payload both itself streams in in chunks and references spots in the Virtual DOM where Promises will later be resolved. In this way React and RSC-supporting meta-frameworks endeavor to improve both real and perceived performance for the user, letting the user see UI as soon as possible.
Payload 本身以分块形式流式传输,并引用虚拟 DOM 中稍后将解析 Promise 的位置。通过这种方式,React 和支持 RSC 的元框架致力于提高用户的真实和感知性能,让用户尽快看到 UI。
But where does flight data stream to really? It must be somewhere in the React codebase.
但飞行数据流到底流向哪里呢?它肯定在 React 代码库的某个地方。
Giving React the Payload
将有效载荷传递给 React
To support RSCs, React added to its codebase the ability to accept the Flight format (a string) and convert it to React Elements in functions like parseModelString
.
为了支持 RSC,React 在其代码库中添加了接受 Flight 格式(一种字符串)并将其转换为 React 元素的功能,例如在 parseModelString
等函数中。
It's up to the RSC-supporting meta-framework to execute those React APIs, sending the appropriate data.
这取决于支持 RSC 的元框架来执行这些 React API,并发送适当的数据。
For example, Next.js adds some extra wrapping components to your app, where it passes in a stream of Payload data.
例如,Next.js 向您的应用程序添加了一些额外的包装组件,在其中传递 Payload 数据流。
It looks like this:
它看起来像这样:
<ServerRoot>
<AppRouter
actionQueue={actionQueue}
globalErrorComponentAndStyles={initialRSCPayload.G}
assetPrefix={initialRSCPayload.p}
/>
</ServerRoot>
Next.js adds a component to your component tree, above the AppRouter
called ServerRoot
. From there it streams to AppRouter
the RSC Payload data.
Next.js 向你的组件树添加了一个组件,位于 AppRouter
之上,称为 ServerRoot
。从那里它将 RSC 有效载荷数据流式传输到 AppRouter
。
Ultimately that data is streamed to React's Promise-based APIs for accepting the Flight format.
最终,这些数据被流式传输到 React 基于 Promise 的 API 中,以接受 Flight 格式。
Thus React provides APIs for building its Virtual DOM from the Payload, and Next.js (or any RSC-supporting meta-framework) has its own mechanisms for getting that data to React after components execute on the server.
因此,React 提供了从 Payload 构建其虚拟 DOM 的 API,而 Next.js(或任何支持 RSC 的元框架)则拥有自己的机制,在组件在服务器上执行后将这些数据传递给 React。
Out-of-Order Streaming 乱序流处理
There's more to the streaming story though. Different components may complete executing at different times. As Payload chunks stream in, how does React know where in the Virtual DOM (and thus the DOM) to place them?
不过,流式传输的故事还有更多内容。不同的组件可能在不同的时间完成执行。当有效载荷块流入时,React 如何知道在虚拟 DOM(以及 DOM)中的哪个位置放置它们?
If we look at the DOM again where we use our DelayedMessage
component, it looks like this at first:
如果我们再次查看使用 DelayedMessage
组件的 DOM,起初它看起来是这样的:
<main>
<h1>understandingreact.com</h1>
<!--$?-->
<template id="B:0"></template>
<p>Loading...</p>
<!--/$-->
</main>
React leaves placeholders like template
with a special ID and HTML comments to note where content should be dropped once the Promise Suspense
is waiting for resolves.
React 使用特殊 ID 和 HTML 注释留下占位符,如 template
,以标记在等待 Promise Suspense
解析时应插入内容的位置。
The fallback is in the DOM, but when the Promise resolves some new JavaScript is streamed to the page:
回退方案在 DOM 中,但当 Promise 解析时,一些新的 JavaScript 会被流式传输到页面:
$RC = function(b, c, e) {
c = document.getElementById(c);
c.parentNode.removeChild(c);
var a = document.getElementById(b);
if (a) {
b = a.previousSibling;
if (e)
b.data = "$!",
a.setAttribute("data-dgst", e);
else {
e = b.parentNode;
a = b.nextSibling;
var f = 0;
do {
if (a && 8 === a.nodeType) {
var d = a.data;
if ("/$" === d)
if (0 === f)
break;
else
f--;
else
"$" !== d && "$?" !== d && "$!" !== d || f++
}
d = a.nextSibling;
e.removeChild(a);
a = d
} while (a);
for (; c.firstChild; )
e.insertBefore(c.firstChild, a);
b.data = "$"
}
b._reactRetry && b._reactRetry()
}
}
;
$RC("B:0", "S:0")
This code inserts the new pieces of the DOM created after the Promise resolves in the right place, where the placeholder was left, and removes the placeholder and fallback.
此代码在 Promise 解析后,将新创建的 DOM 片段插入到正确的位置,即占位符所在之处,并移除占位符和回退内容。
After this DOM manipulation code is run, the DOM looks like this:
运行此 DOM 操作代码后,DOM 看起来像这样:
<main>
<h1>understandingreact.com</h1>
<!--$-->
<p>This message was loaded after a 5 second delay!</p>
<!--/$-->
</main>
This is called out-of-order streaming and simply means taking what is streamed in and inserting it in the right, expected place in the Virtual DOM/DOM tree, even if its expected spot is before other components that finish first.
这被称为乱序流式传输,简单来说就是将流式传输的内容插入到虚拟 DOM/DOM 树中正确、预期的位置,即使其预期位置在其他先完成的组件之前。
In this way if one particular component takes longer than others to execute, you don't have to wait for it to update the UI with the results of other components.
这样一来,如果某个特定组件的执行时间比其他组件长,您无需等待它更新 UI 以显示其他组件的结果。
So far, however, we've only been concerning ourselves with Server Components. What about the components devs have been writing for years? What about Client Components, functions that execute in the browser?
然而,到目前为止,我们只关注了服务器组件。那么开发者们多年来编写的组件呢?那些在浏览器中执行的客户端组件和函数又该如何处理?
The answer introduces an unsung hero in the RSC story: bundlers.
答案揭示了 RSC 故事中一位默默无闻的英雄:打包工具。
Bundlers and Interleaving
打包器与交错加载
One of React's core tenets has always been component composition. You can split the work of deciding what the DOM should look like across many functions, and compose (that is, combine) that work together by having components be children of each other.
React 的核心原则之一始终是组件组合。你可以将决定 DOM 外观的工作拆分到多个函数中,并通过让组件相互嵌套来组合(即结合)这些工作。
For RSCs to not be a dramatic shift of this core tenet, you need to be able to interleave (or weave) Server and Client Components. Client Components need to be able to be children of Server Components. That includes being able to pass props (function arguments).
为了使 RSC 不会对这一核心原则造成剧烈转变,你需要能够交错(或编织)服务器组件和客户端组件。客户端组件需要能够作为服务器组件的子组件。这包括能够传递 props(函数参数)。
What that really means is that in your component hierarchy some of your functions will run on the server and some on the client. In the end though, they all will be doing work that calculates how the part of the DOM that they generate should be structured and what it should contain.
这意味着在你的组件层次结构中,部分函数将在服务器端运行,而另一部分则在客户端运行。但最终,它们都将执行计算工作,确定它们生成的 DOM 部分应如何构建以及应包含什么内容。
It is the responsibility of the meta-frameworks and bundlers to make this happen, and they do. But remember abstractions have a cost. Often that cost is special rules you have to learn to use the abstraction. In this case, abstracting away some of the separation of server and client means following rules to prevent breaking the limitations of the abstraction.
这是元框架和打包器的责任,它们也确实做到了。但请记住,抽象是有代价的。通常,这个代价是你必须学习使用抽象的特殊规则。在这种情况下,抽象掉部分服务器和客户端之间的分离意味着要遵循规则,以防止破坏抽象的限制。
In the case of RSCs there are 3 interleaving scenarios to consider. The rules are, really, about what can be imported by the component depending on where it will execute. They are rules based on how RSCs work along with the bundlers who analyze the directives and imports and pull it all together.
对于 RSC(React Server Components)而言,存在三种交织的场景需要考虑。这些规则实质上是关于组件根据其执行位置可以导入哪些内容。它们基于 RSC 的工作原理,以及分析指令和导入并将其整合在一起的打包工具。
Client Components Imported Into Server Components
客户端组件导入到服务器组件
This is allowed. It makes sense that this is fine. Bundlers look at import statements to decide which code to include in their bundles, and which code will be downloaded by the client.
这是允许的。这样做是有道理的。打包工具会查看导入语句来决定哪些代码要包含在它们的包中,以及哪些代码将由客户端下载。
RSCs also participate in building the Virtual DOM. They can reference Client Components in their trees, because that Client Component code will be made available to the browser in the bundle.
RSCs 也参与构建虚拟 DOM。它们可以在其树中引用客户端组件,因为该客户端组件代码将在打包后的文件中提供给浏览器。
Let's try adding a stateful Counter
to our React course enrollment page:
让我们尝试在我们的 React 课程注册页面中添加一个有状态的 Counter
:
// components/Counter.js
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>
Enroll
</button>
</section>
);
}
// page.js
import Counter from "./components/Counter";
import DelayedMessage from "./components/DelayedMessage";
import { Suspense } from "react";
export default function Home() {
return (
<main>
<h1>understandingreact.com</h1>
<Counter />
<Suspense fallback={<p>Loading...</p>}>
<DelayedMessage />
</Suspense>
</main>
);
}
Notice the use client
directive at the top of the file. This is not a React feature. It's an agreed upon convention for devs to mark that a portion of the component tree is meant to be executed on the client.
注意文件顶部的 use client
指令。这不是 React 的特性。这是开发者之间约定俗成的惯例,用于标记组件树的某一部分应在客户端执行。
That component and any components it imports will be bundled as Client Components.
该组件及其导入的任何组件都将作为客户端组件进行打包。
The bundler will note the use client
directive and include that component's code (and any it imports) in what is downloaded by the browser.
打包工具会注意到 use client
指令,并将该组件的代码(以及它导入的任何代码)包含在浏览器下载的内容中。
The Home
and DelayedMessage
RSCs will execute on the server, and their code won't be included in the bundle. The Payload from the server will look like this:
Home
和 DelayedMessage
的 RSCs 将在服务器上执行,它们的代码不会包含在打包文件中。来自服务器的 Payload 将如下所示:
{
"type": "main",
"key": null,
"props": {
"children": [
{
"type": "h1",
"key": null,
"props": {
"children": "understandingreact.com"
}
},
{
"type": {
"$$type": "reference",
"id": "d",
"identifier": "L",
"type": "Lazy node"
},
"key": null,
"props": {}
},
{
"type": {
"$$type": "reference",
"id": "e",
"identifier": "",
"type": "Reference"
},
"key": null,
"props": {
"fallback": {
"type": "p",
"key": null,
"props": {
"children": "Loading..."
}
},
"children": {
"$$type": "reference",
"id": "f",
"identifier": "L",
"type": "Lazy node"
}
}
}
]
}
}
Notice there's a new "Lazy node" reference where the Client Component will be. That part of the Virtual DOM will be known when that Client Component executes. That will happen either when the Client Component is SSR'd (if the framework does that) or when it executes in the browser.
请注意,在客户端组件的位置有一个新的“懒节点”引用。虚拟 DOM 的那部分将在客户端组件执行时被知晓。这将在客户端组件进行 SSR(如果框架支持)或在浏览器中执行时发生。
One more note: if you pass props from a Server Component to a Client Component, those props need to be serializable by React.
还有一个注意事项:如果你从服务器组件传递 props 到客户端组件,这些 props 需要能够被 React 序列化。
As we've seen, the props will be part of the Payload sent over the network. That means anything passed needs to be representable as a string, so it can be converted back into an object in memory on the client.
正如我们所看到的,props 将成为通过网络发送的 Payload 的一部分。这意味着传递的任何内容都需要能够表示为字符串,以便可以在客户端的内存中转换回对象。
Server Components Imported Into Client Components
服务器组件导入客户端组件
This isn't allowed. You can't import a component that is intended to run on the server into your component that will run in the browser.
这是不允许的。你不能将旨在服务器上运行的组件导入到将在浏览器中运行的组件中。
Why? Because bundlers shouldn't send RSC functions to the client, only the Payload. Therefore there is no code to import. The bundler won't include the code for the client to download, so the RSC code isn't there to use.
为什么?因为打包工具不应该将 RSC 函数发送到客户端,只应发送 Payload。因此,没有代码需要导入。打包工具不会包含供客户端下载的代码,所以 RSC 代码并不存在以供使用。
It is possible to import a shared component that is viable to run on both the server and client. But if you import a shared component into a Client Component then its code will be bundled for download by the client. If you import a shared component into a Server Component, then it won't be bundled.
可以导入一个既能在服务器端也能在客户端运行的共享组件。但如果你将共享组件导入到客户端组件中,那么它的代码将会被打包供客户端下载。如果你将共享组件导入到服务器端组件中,则不会被打包。
You might thinking: "what if I accidentally import a Server Component, how does the bundler know I don't mean to?"
你可能会想:“如果我不小心导入了一个服务器组件,打包工具怎么知道我不是故意的呢?”
Good question! This is a bit of a security problem. You could have code in a Server Component that is never meant to be downloaded and seen by others, but you accidentally import it into a Client Component and so it gets bundled in. If it has server-specific features (like connecting to a database) it will fail to execute in the browser, but if it made it into production you might have leaked some sensitive information like the address of your database.
好问题!这确实有点安全隐患。你可能在服务器组件中编写了一些代码,本意是不希望被下载或让他人看到,但你不小心将其导入到了客户端组件中,结果它就被打包进去了。如果这些代码包含服务器特有的功能(比如连接数据库),在浏览器中执行时会失败,但如果它进入了生产环境,你可能会泄露一些敏感信息,比如数据库的地址。
Next.js tries to resolve this by allowing you to mark components as server only. This is a bit like tying string to your finger to remember something though. It's possible to forget to tie the string.
Next.js 试图通过允许你将组件标记为仅服务器端来解决这个问题。这有点像在手指上系绳子来提醒自己某件事。但有可能忘记系绳子。
Other meta-frameworks are looking at safer alternatives for ensuring your server code doesn't get bundled and sent to the client.
其他元框架正在寻找更安全的替代方案,以确保您的服务器代码不会被捆绑并发送到客户端。
However, once you accept an abstraction over the server-client boundary, you accept a degree of risk in forgetting those boundaries exist.
然而,一旦你接受了跨越服务器-客户端边界的抽象,就意味着你承担了一定程度的风险,即可能会忘记这些边界的存在。
Server Components Passed as Children to Client Components
作为子组件传递给客户端组件的服务器组件
This is allowed. This is a special, interesting case. You can pass Server Components as children
props to a Client Component, which is different from importing it.
这是允许的。这是一个特殊且有趣的案例。你可以将服务器组件作为 children
属性传递给客户端组件,这与直接导入它是不同的。
If we gave our Counter
function some children:
如果我们给我们的 Counter
函数一些子函数:
// components/Counter.js
'use client';
import { useState } from 'react';
export default function Counter({ children }) {
const [count, setCount] = useState(0);
return (
<section>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>
Enroll
</button>
{ children }
</section>
);
}
// page.js
import Counter from "./components/Counter";
import DelayedMessage from "./components/DelayedMessage";
import { Suspense } from "react";
export default function Home() {
return (
<main>
<h1>understandingreact.com</h1>
<Counter>
<p>Server Text</p>
</Counter>
<Suspense fallback={<p>Loading...</p>}>
<DelayedMessage />
</Suspense>
</main>
);
}
It works just fine, even though the Counter
function executes on the client, and the children passed to it (<p>Server Text</p>
) were processed on the server!
它运行得很好,尽管 Counter
函数在客户端执行,而传递给它的子元素( <p>Server Text</p>
)是在服务器端处理的!
Why does this work? Because what you're really passing is a portion of the Virtual DOM tree (the results of executing the code), not the Server Component code to be executed.
为什么这能行得通?因为你实际传递的是虚拟 DOM 树的一部分(执行代码的结果),而不是要执行的服务器组件代码。
The Payload looks like this:
Payload 看起来像这样:
{
"children": [
{
"type": "h1",
"key": null,
"props": {
"children": "understandingreact.com"
}
},
{
"type": {
"$$type": "reference",
"id": "d",
"identifier": "L",
"type": "Lazy node"
},
"key": null,
"props": {
"children": {
"type": "p",
"key": null,
"props": {
"children": "Server Text"
}
}
}
},
{
"type": {
"$$type": "reference",
"id": "e",
"identifier": "",
"type": "Reference"
},
"key": null,
"props": {
"fallback": {
"type": "p",
"key": null,
"props": {
"children": "Loading..."
}
},
"children": {
"$$type": "reference",
"id": "f",
"identifier": "L",
"type": "Lazy node"
}
}
}
]
}
}
Notice the "Server Text" portion of the Payload. It's already passed as a prop to the Client Component. It's as if you'd simply written the JSX in the Payload directly in your Client Component.
注意 Payload 中的 "Server Text" 部分。它已经作为 prop 传递给了客户端组件。这就像你直接在客户端组件中编写了 Payload 中的 JSX 一样。
Bundlers: The Unsung Heroes
打包工具:默默无闻的英雄
All of this serves to show an important point. React Server Components, in many ways, are a bundler feature.
这一切都表明了一个重要观点。React 服务器组件,在许多方面,是一个打包工具的特性。
The bundler analyzes your code, and ensures that Client Components are in the bundle and helps ensure references to those Client Components appear properly in the Payload.
打包器会分析你的代码,确保客户端组件包含在包中,并帮助确保这些客户端组件的引用在有效载荷中正确显示。
Bundlers are a first-class citizen in React. If you look at the React codebase you'll find folders like:
打包工具在 React 中是一等公民。如果你查看 React 的代码库,你会发现类似以下的文件夹:
/react-server-dom-parcel
/react-server-dom-turbopack
/react-server-dom-webpack
//...and more
Inside those folders are code having to do with Flight, helping the bundled code get all of this right.
这些文件夹中的代码与 Flight 相关,帮助捆绑代码正确处理所有内容。
Because bundlers are the unsung hero of RSCs, it also means that other conventions are possible. A meta-framework doesn't have to buy-in to the use client
approach that Next.js uses. TanStack Start, for example, is implementing RSCs simply as functions that "return JSX" (i.e. the Flight format).
由于打包器是 RSC 的无名英雄,这也意味着其他约定也是可能的。元框架不必采用 Next.js 使用的 use client
方法。例如,TanStack Start 将 RSC 简单地实现为“返回 JSX”的函数(即 Flight 格式)。
React has provided an API: streaming Flight data. It's up the meta-frameworks to iterate and innovate on how they use that API.
React 提供了一个 API:流式传输 Flight 数据。元框架需要在此基础上迭代和创新,探索如何使用该 API。
Hooks and RSCs 钩子和 RSCs
Execution on the server comes with some advantages, but also some limitations.
在服务器上执行有一些优势,但也有一些限制。
React doesn't just store the structure of elements in the Virtual DOM, it stores state. When you write:
React 不仅仅在虚拟 DOM 中存储元素的结构,它还存储状态。当你编写:
const [counter, setCounter] = useState(0);
in a component, it places that data in a node on a linked list attached to your component's place in the Virtual DOM. In reality, then, that state is sitting in a JavaScript object in the client browser's memory.
在组件中,它将数据放置在虚拟 DOM 中与组件位置相关联的链表节点上。实际上,该状态位于客户端浏览器内存中的 JavaScript 对象中。
So, React Server Components, by their very nature, can't use those hooks. They run in the wrong environment to do that.
因此,React 服务器组件本质上无法使用这些钩子。它们在错误的环境中运行,无法实现这一点。
That means, ultimately, that RSCs are non-interactive. Interactivity in React generally means triggering a client-side React re-render, and that happens by updating state.
这意味着,最终,RSCs 是非交互式的。在 React 中,交互性通常意味着触发客户端的 React 重新渲染,而这通过更新状态来实现。
This means that as your app gains more and more interactive functionality, you tend to refactor your Server Components into Client and Server Component compositions.
这意味着随着您的应用程序获得越来越多的交互功能,您倾向于将服务器组件重构为客户端和服务器组件的组合。
Whenever you need something like useReducer
or useState
, you need a Client Component.
每当您需要类似 useReducer
或 useState
的内容时,您需要一个客户端组件。
If you keep in mind where your components are executing, you'll use (or not use) Hooks properly.
如果你牢记组件在哪里执行,你就会正确使用(或不使用)Hooks。
To Hydrate or Not to Hydrate
水合与否
I wanted to mention for a moment a point of common confusion. Do RSCs hydrate?
我想提一下一个常见的困惑点。RSCs 会水合吗?
The answer is no. Hydration is about re-executing the actual functions on the client, in order to build the Virtual DOM so that events can be hooked to their handlers.
答案是否定的。水合作用是指在客户端重新执行实际的函数,以构建虚拟 DOM,从而将事件绑定到其处理程序上。
In React, when we click a button, that event is sent way up the DOM tree to React's root, where React then determines which component should handle the click (the answer is the component that was responsible for creating the button).
在 React 中,当我们点击一个按钮时,该事件会沿着 DOM 树向上传递到 React 的根节点,然后 React 会确定哪个组件应该处理这个点击事件(答案是负责创建该按钮的组件)。
Thus you need the Virtual DOM in place and the code that handles the click so that React can respond properly to the event.
因此,你需要准备好虚拟 DOM 以及处理点击事件的代码,以便 React 能够正确响应事件。
RSCs are non-interactive. They won't set state, they won't handle clicks, at least not via React's normal approach. Their code isn't sent to the client for execution, so by definition they don't hydrate.
RSCs 是非交互式的。它们不会设置状态,也不会处理点击事件,至少不会通过 React 的常规方式。它们的代码不会发送到客户端执行,因此从定义上讲,它们不会进行水合。
However, they are part of building the Virtual DOM. They are part of tree reconciliation. The fact that they don't hydrate doesn't mean they aren't in the tree during hydration. They are.
然而,它们是构建虚拟 DOM 的一部分。它们是树协调的一部分。它们不进行水合并不意味着在水合过程中它们不在树中。它们确实在。
Refetching and Reconciliation
重新获取与协调
In real world apps you likely are not not just concerned with the initial load of a page, but concerned with refetching those server components.
在实际应用中,您可能不仅关注页面的初始加载,还关注这些服务器组件的重新获取。
That means asking the server to re-execute those components (perhaps with new props), and provide new Payload data to update the Virtual DOM with.
这意味着请求服务器重新执行这些组件(可能使用新的属性),并提供新的有效载荷数据以更新虚拟 DOM。
For example, if we are paginating through a list of data, and that list is generated by an RSC, we want to get a different set of data if the route is /page/1
versus /page/2
.
例如,如果我们正在对数据列表进行分页,并且该列表由 RSC 生成,我们希望根据路由是 /page/1
还是 /page/2
获取不同的数据集。
This is an advantage of RSCs, and most likely integrated with the meta-frameworks' router. The meta-framework doesn't have to do a full page refresh, even though the UI is calculated on the server.
这是 RSCs 的一个优势,并且很可能与元框架的路由器集成。元框架无需进行完整的页面刷新,即使 UI 是在服务器上计算的。
By its very nature, RSCs can stream Virtual DOM definitions to the client, and React can then perform client-side reconciliation as normal. In other words, the page doesn't have to refresh and you don't lose other state on the page.
RSC 的本质决定了它能够将虚拟 DOM 定义流式传输到客户端,然后 React 可以像往常一样执行客户端协调。换句话说,页面无需刷新,也不会丢失页面上的其他状态。
In this aspect RSCs can provide the best of both rendering worlds. They can run on the server, but update as if they were executed on the client.
在这方面,RSC 可以提供两种渲染世界的最佳结合。它们可以在服务器上运行,但更新时就像在客户端执行一样。
Now that we've covered how RSCs really work, this hopefully makes more intuitive sense. React already updates the DOM by diffing the Virtual DOM. So if we can get Virtual DOM data from the server, it can do what it always has.
既然我们已经介绍了 RSC 的实际工作原理,希望这能让大家更直观地理解。React 已经通过对比虚拟 DOM 来更新 DOM。因此,如果我们能从服务器获取虚拟 DOM 数据,它就能像往常一样工作。
The Bundle Size Confusion
捆绑包大小的困惑
For over a decade now, I've pushed having deep understanding how the tools you use work. That's what my courses have always been about.
十多年来,我一直倡导深入理解你所使用工具的工作原理。这也是我的课程始终围绕的核心内容。
I'm happy that so many students have appreciated that approach. Yet, every now and again, someone complains "too much theory, just learn by doing".
我很高兴这么多学生欣赏这种方法。然而,时不时会有人抱怨“理论太多,应该边做边学”。
One of the major values, however, of understanding how the tools, libraries, and frameworks we use work is we can make good, informed architectural decisions.
然而,理解我们所使用的工具、库和框架如何工作的一个主要价值在于,我们能够做出明智的架构决策。
For example, there's been confusion in the Next.js world on the benefits of RSCs. There's a pretty extraordinary discussion on the next.js code repository about the __next_f()
function.
例如,在 Next.js 社区中,关于 RSCs(React Server Components)的优势存在一些困惑。在 next.js 的代码仓库中,有一个关于 __next_f()
函数的非常特别的讨论。
Devs who started to use RSCs discovered that there was duplicated data being passed to this function in script
tags at the bottom of their pages. Some asked why it was there and if it could be turned off.
开始使用 RSC 的开发者们发现,在他们的页面底部的 script
标签中,有重复的数据被传递给了这个函数。一些人询问为什么会有这种情况,以及是否可以关闭它。
What is this doubled data? You guessed it. The Payload! Those function calls that were streamed in ultimately pass that Payload data to React to create the Virtual DOM. This is shocking if you don't understand how all this works.
这些重复的数据是什么?你猜对了。就是 Payload(有效载荷)!那些被流式传输进来的函数调用最终会将 Payload 数据传递给 React,以创建虚拟 DOM。如果你不了解这一切是如何运作的,这确实令人震惊。
The issue is that it increases bandwidth usage, which many were complaining about. You're sending more data across the network.
问题在于它增加了带宽使用量,许多人对此表示不满。你正在通过网络发送更多数据。
Why were people surprised? Well, Vercel originally described RSCs this way on Next.js' documentation site:
为什么人们会感到惊讶?因为 Vercel 最初在 Next.js 的文档网站上这样描述 RSCs:
The phrase "the client down not have download, parse, and execute any JavaScript for Server Components" was the misleading one. It isn't really true. Vercel was referring to the actual JavaScript code of the Server Components, but they used the word any.
短语“客户端无需下载、解析和执行任何用于服务器组件的 JavaScript”是具有误导性的。这并不完全正确。Vercel 实际上指的是服务器组件的实际 JavaScript 代码,但他们使用了“任何”这个词。
I had an interesting branching conversation with them on social media. I like to think that was part of the reason the wording was later changed (kudos to Vercel for changing it):
我在社交媒体上与他们进行了一次有趣的分支对话。我喜欢认为这是后来措辞改变的部分原因(感谢 Vercel 做出了更改):
The new description says that Server Components "can reduce the amount of client-side JavaScript needed". This is true! But they can also increase it, because the Payload, in a sense, is JavaScript, or at least data passed to JavaScript functions.
新的描述指出,服务器组件“可以减少所需的客户端 JavaScript 数量”。这是事实!但它们也可能增加它,因为从某种意义上说,有效负载是 JavaScript,或者至少是传递给 JavaScript 函数的数据。
The core misunderstanding of devs is that bundle size and bandwidth usage are not the same thing.
开发者的核心误解在于,打包大小和带宽使用量并不是一回事。
Server Component code is not in the bundle! So they do reduce bundle size! But the Payload is doubled data, and if that data is large (like a big blog post like this one) you'll end up sending more bytes than you save.
服务器组件代码不在打包文件中!因此它们确实减少了打包体积!但数据负载会翻倍,如果数据量很大(比如像这样一篇长篇博客文章),最终发送的字节数可能会超过节省的部分。
When Should You Use RSCs?
何时应使用 RSCs?
So, when should you use RSCs? The right answer, as always, is "it depends". Being armed with an accurate mental model of how they work will help you make that architectural choice.
那么,何时应该使用 RSCs 呢?一如既往,正确的答案是“视情况而定”。掌握它们如何运作的准确心智模型,将有助于你做出架构选择。
For my part, I wouldn't use RSCs for a big blog post like this one. The bandwidth usage doesn't make sense to me. I'd use something like Astro for content-heavy sites and apps.
就我而言,我不会为像这样的大型博客文章使用 RSCs。带宽使用对我来说没有意义。我会为内容丰富的网站和应用使用像 Astro 这样的工具。
On the other hand, if I had a lot of DB access and complex logic, I might offload that to the server by doing it on a Server Component. The same if I needed to use large JavaScript libraries to produce a relatively small amount of content. If the bundle to Payload trade off is worth it, RSCs make sense to me.
另一方面,如果我需要进行大量的数据库访问和复杂逻辑处理,我可能会通过服务器组件将其卸载到服务器上执行。同样地,如果我需要使用大型 JavaScript 库来生成相对较少的内容,如果打包大小与有效负载之间的权衡是值得的,那么使用 RSCs(服务器组件)对我来说是有意义的。
If I have a highly interactive app, and I'm iterating and constantly adding features, I'd also be hesistant to do too much client/server refactoring and might keep it as mostly Client Components.
如果我有一个高度交互的应用程序,并且我正在迭代并不断添加功能,我也会犹豫是否进行过多的客户端/服务器重构,可能会将其保留为主要是客户端组件。
There are simply too many variables to make a hard recommendation. The best I can do is what I tried to do here in this post: help you understand, so you can come to an informed decision.
有太多的变量无法给出一个明确的建议。我所能做的,就是尽力在这篇文章中帮助你理解,以便你能做出明智的决定。
Looking Forward 展望未来
What's the future of React Server Components? It isn't entirely clear. React has made the API, and meta-frameworks are using it.
React 服务器组件的未来是什么?目前还不完全清楚。React 已经提供了 API,而元框架正在使用它。
I think the TanStack Start approach of functions that return Virtual DOM, rather than full Server Components, will be popular. But for some uses, Next.js' approach will work well.
我认为 TanStack Start 采用返回虚拟 DOM 的函数方法,而非完整的服务器组件,将会受到欢迎。但对于某些用途,Next.js 的方法也会表现良好。
I hope to see incremental improvements in security and performance. For example, if a branch of the Virtual DOM is all Server Components, some reconciliation or hydration optimizations could be made to skip that part of the tree.
我希望看到安全和性能方面的逐步改进。例如,如果虚拟 DOM 的一个分支全是服务器组件,可以对该树的那部分进行一些协调或水合优化,以跳过该部分。
Diving Deeper 深入探索
I hope you found this post useful to your understanding of React Server Components. For a deep dive like this into all of React, from scratch, you can enroll in my full course Understanding React.
希望这篇文章能帮助你更好地理解 React 服务器组件。如果你想从零开始深入学习 React 的各个方面,可以报名参加我的完整课程《理解 React》。
Over 27 modules and 16.5 hours of video content we dive into React's source code together, to build the most valuable tool in a developer's toolbelt: an accurate mental model.
超过 27 个模块和 16.5 小时的视频内容,我们将一起深入 React 的源代码,构建开发者工具带中最有价值的工具:一个准确的心智模型。
Happy coding! 编程愉快!