「译」为什么 RSC 才是正确答案?

633 阅读20分钟

原文:www.builder.io/blog/why-re…

标题:Why React Server Components Are Breaking Builds to Win Tomorrow

作者:builder.io

image.png

在过去的十年中,React 及其生态系统经历了不断的发展。每个版本都引入了新的概念、优化,有时甚至是范式转变,突破了我们认为 Web 开发可能的界限。

React 服务器组件 (RSC) 是自 React Hooks以来的最新变化,也许是最重要的变化。然而,这一变化在社区内引起了不同的反响。

对我来说,Linkin Park 的这句话很好地总结了我们步入 2024 年时围绕 React 演变的情绪态度:

一旦你有了关于事物如何运作的理论,每个人都希望下一件事和第一件事一样

我们已经习惯了我们所熟悉和喜爱的 React,可以理解的是,拥抱范式的转变会带来充满犹豫和怀疑的挑战。

这篇博文的目的是引导你了解 React 多年来的渲染演变历程,并帮助你理解为什么 React 服务器组件(RSC)不仅是不可避免的,而且是构建具有成本效益的高性能 React 应用程序的未来,这些应用程序可以提供卓越的用户体验。

客户端渲染 (CSR)

如果你已经在前端开发中工作了一段时间,你会知道 React 是创建单页应用程序 (SPA) 的首选库。

在典型的 SPA 中,当客户端发出请求时,服务器会向浏览器(客户端)发送单个 HTML 页面。此 HTML 页面通常只包含一个简单的 div 标记,即对 JavaScript 文件的引用。此 JavaScript 文件包含应用程序运行所需的所有内容,包括 React 库本身和应用程序代码。解析 HTML 文件时下载它。

然后,下载的 JavaScript 代码会在你的计算机上生成 HTML,并将其插入到 DOM 的根 div 元素下,你会在浏览器中看到用户界面。

当你看到 HTML 出现在 DOM 检查器中,但未出现在“查看源代码”选项中时,此过程是显而易见的,该选项显示服务器发送到浏览器的 HTML 文件。

这种呈现方法,其中组件代码直接在浏览器(客户端)中转换为用户界面,称为客户端呈现 (CSR)。

下面是客户端渲染的可视化效果:

编辑器

下面是 React SPA 的 DOM 检查器与页面源的对比:

CSR DOM vs Source

CSR 很快成为 SPA 的标准并得到广泛采用。然而,不久之后,开发人员就开始注意到这种方法的一些固有缺陷。

CSR的缺点

首先,生成主要包含单个 div 标签的 HTML 对于 SEO 来说并不是最佳选择,因为它为搜索引擎提供的索引内容很少。较大的包大小和来自深度嵌套组件的 API 响应的网络请求瀑布可能会导致有意义的内容无法以足够快的速度呈现,以便爬虫对其进行索引。

其次,让浏览器(客户端)处理所有工作,例如获取数据、计算 UI 以及使 HTML 交互,可能会减慢速度。页面加载时,用户可能会看到空白屏幕或加载旋转图标。随着时间的推移,这个问题往往会变得更糟,因为添加到应用程序的每个新功能都会增加 JavaScript 包的大小,从而延长用户查看 UI 的等待时间。对于互联网连接速度较慢的用户来说,这种延迟尤其明显。

CSR 为我们今天习惯的交互式 Web 应用程序奠定了基础,但为了增强 SEO 和性能,开发人员开始寻找更好的解决方案。

服务器端渲染 (SSR)

为了克服 CSR 的缺点,Next.js 等现代 React 框架转向服务器端解决方案。这种方法从根本上改变了向用户交付内容的方式。

服务器负责呈现完整的 HTML,而不是发送依赖于客户端 JavaScript 来构建页面的几乎空的 HTML 文件。然后,这个完整的 HTML 文档将直接发送到浏览器。由于 HTML 是在服务器上生成的,因此浏览器能够快速解析并显示它,从而缩短了初始页面加载时间。

这是服务器端渲染的可视化:

2

解决CSR的缺点

服务器端方法有效地解决了与CSR相关的问题。

首先,它显着改善了 SEO,因为搜索引擎可以轻松索引服务器呈现的内容。

其次,浏览器可以立即加载页面 HTML 内容,而不是出现空白屏幕或加载微调框。

水合 Hydration

SSR 立即提高内容可见性的方法有其自身的复杂性,特别是在页面的交互性方面。页面的完整交互性将被暂停,直到 JavaScript 包(包括 React 本身以及应用程序特定代码)已被浏览器完全下载并执行。

这个重要的阶段称为水合作用,是最初由服务器提供的静态页面被赋予生命的阶段。在水合过程中,React 控制浏览器,根据所提供的静态 HTML 重建内存中的组件树。它仔细规划了树中交互元素的放置。

然后,React 继续将必要的 JavaScript 逻辑绑定到这些元素。这涉及初始化应用程序状态、为单击和鼠标悬停等操作附加事件处理程序,以及设置完全交互式用户体验所需的任何其他动态功能。

SSG 和 SSR

更深入地研究,服务器端解决方案可以分为两种策略:静态站点生成(SSG)和服务器端渲染(SSR)。

SSG 在构建时发生,即应用程序部署在服务器上时。这会导致页面已经呈现并准备好提供服务。它非常适合不经常更改的内容,例如博客文章。

另一方面,SSR 按需渲染页面以响应用户请求。它适用于社交媒体提要等个性化内容,其中 HTML 取决于登录用户。通常,你会看到两者统称为服务器端渲染或 SSR。

服务器端渲染 (SSR) 是对客户端渲染 (CSR) 的重大改进,提供更快的初始页面加载和更好的 SEO。然而,SSR 也带来了自己的一系列挑战。

SSR 的缺点

SSR 的一个问题是组件会被阻塞渲染,因为数据仍在”加载”or“等待”。如果组件需要从数据库或其他来源(如 API)获取数据,则必须在服务器开始呈现页面之前完成此获取。这可能会延迟服务器对浏览器的响应时间,因为服务器必须先完成所有必要数据的收集,然后才能将页面的任何部分发送到客户端。

SSR 的第二个问题是,为了成功实现水合作用,React 向服务器渲染的 HTML 添加交互性,浏览器中的组件树必须与服务器生成的组件树完全匹配。这意味着组件的所有 JavaScript 都必须先加载到客户端,然后才能开始对其中任何组件进行 Hydrating。

SSR 的第三个问题与水合作用本身有关。 React 一次性水合组件树,这意味着一旦开始水合,它就不会停止,直到完成整个树。因此,所有成分都必须先水合,然后才能与其中任何成分相互作用。

这三个问题——必须加载整个页面的数据、加载整个页面的 JavaScript 以及水合整个页面——创建了一个从服务器到客户端的全有或全无的瀑布问题,其中每个问题都必须在进行下一个之前先解决。如果应用程序的某些部分比其他部分慢(现实应用程序中经常出现这种情况),那么这种方法的效率就会很低。

由于这些限制,React 团队引入了一种新的、改进的 SSR 架构。

SSR 的 Suspense

React 18 为 SSR 引入了 Suspense,以解决传统 SSR 的性能缺陷。这个新架构允许使用组件来解锁两个主要的 SSR 功能:

  1. 服务器上的 HTML 流式传输
  2. 为客户选择性水合

服务器上的 HTML 流式传输

正如我们在上一节中讨论的,传统上,SSR 是一件要么全有要么全无的事情。服务器呈现完整的 HTML,然后将其发送到客户端。客户端显示此 HTML,只有在加载完整的 JavaScript 包后,React 才会继续水合整个应用程序以添加交互性。

以下是上述过程的可视化:

html

然而,在 React 18 中,我们有了新的可能性。通过将页面的一部分(例如主要内容区域)包装在 React Suspense 组件中,我们指示 React 不需要等待主要部分数据被获取即可开始流式传输页面其余部分的 HTML。 React 将发送一个占位符,例如加载旋转器,而不是完整的内容。

一旦服务器准备好主要部分的数据,React 就会通过正在进行的流发送额外的 HTML,并附带一个内联 <script> 标签,其中包含正确定位该 HTML 所需的最少 JavaScript。因此,即使在客户端加载完整的 React 库之前,主要部分的 HTML 对用户也是可见的。

以下是使用 <Suspense> 的 HTML 流的可视化:

这解决了我们的第一个问题。在显示任何内容之前,你不必获取所有内容。如果特定部分延迟了初始 HTML,则可以稍后将其无缝集成到流中。这就是 <Suspense> 促进服务器端 HTML 流的本质。

为客户端选择性水合

虽然我们现在可以加快初始 HTML 交付速度,但我们仍然面临另一个挑战。在加载主要部分的 JavaScript 之前,客户端应用程序水合作用无法启动。如果主要部分的 JavaScript 包很大,则可能会严重延迟该过程。

为了缓解这种情况,可以使用代码分割。代码分割意味着你可以将特定的代码段标记为不立即需要加载,从而指示你的捆绑程序将它们分隔成单独的 <script> 标记。

使用 React.lazy 进行代码拆分使你能够将主要部分的代码与主要 JavaScript 包分开。因此,包含 React 的 JavaScript 以及整个应用程序的代码(不包括主要部分)现在可以由客户端独立下载,而无需等待主要部分的代码。

这一点至关重要,因为通过将主要部分包装在 <Suspense> 中,你已经向 React 表明它不应阻止页面的其余部分不仅流式传输,而且还阻止水合。这个称为选择性水合的功能允许在完全下载其余 HTML 和 JavaScript 代码之前对可用的部分进行水合。

从用户的角度来看,最初他们获得的是以 HTML 形式传输的非交互式内容。然后你告诉 React 进行水合。主要部分的 JavaScript 代码还没有,但是没关系,因为我们可以有选择地合并其他组件。

一旦加载了代码,主要部分就会被水合。

由于选择性水合作用,大量的 JS 不会妨碍页面的其余部分变得具有交互性。

以下是 <Suspense> 选择性水合的可视化:

5

此外,选择性水合为第三个问题提供了解决方案:“水合一切以与任何事物相互作用”的必要性。 React 尽快开始水合,从而可以与标题和侧面导航等元素进行交互,而无需等待主要内容水合。这个过程由 React 自动管理。

在多个组件等待水合作用的情况下,React 根据用户交互优先考虑水合作用。例如,如果侧边栏即将被水合,并且你单击了主要内容区域,React 将在单击事件的捕获阶段同步水合被单击的组件。这确保组件准备好立即响应用户交互。 sidenav 随后会被水合。

以下是基于用户交互的水合可视化:

SSR Suspense 的缺点

首先,即使 JavaScript 代码异步传输到浏览器,最终用户也必须下载网页的整个代码。随着应用程序添加更多功能,用户需要下载的代码量也会增加。这就引出了一个重要的问题:用户真的应该下载这么多数据吗?

其次,当前的方法要求所有 React 组件在客户端进行水合作用,而不考虑它们对交互性的实际需求。此过程可能会低效地消耗资源并延长加载时间和用户交互时间,因为他们的设备需要处理和呈现甚至可能不需要客户端交互的组件。这引出了另一个问题:所有组件都应该水合吗,即使是那些不需要交互性的组件?

第三,尽管服务器具有处理密集处理任务的卓越能力,但大部分 JavaScript 执行仍然发生在用户设备上。这会降低性能,尤其是在功能不是很强大的设备上。这引出了另一个重要问题:这么多工作应该在用户的设备上完成吗?

为了应对这些挑战,仅仅采取渐进的步骤是不够的。我们需要迈向更强大的解决方案的重大飞跃。

React 服务器组件 (RSC)

React Server Components (RSC) 代表了 React 团队设计的新架构。这种方法旨在利用服务器和客户端环境的优势,优化效率、加载时间和交互性。

该架构引入了双组件模型,区分客户端组件和服务器组件。这种区别不是基于组件的功能,而是基于它们执行的位置以及它们设计用于交互的特定环境。让我们仔细看看这两种类型:

客户端组件

客户端组件是我们在之前的渲染技术中一直使用和讨论的熟悉的 React 组件。它们通常在客户端 (CSR) 上呈现,但也可以在服务器 (SSR) 上呈现为 HTML,从而允许用户立即看到页面的 HTML 内容,而不是空白屏幕。

在服务器上呈现的“客户端组件”的想法可能看起来令人困惑,但将它们视为主要在客户端上运行但也可以(并且应该)也可以在服务器上执行一次作为优化策略的组件是有帮助的。

客户端组件可以访问客户端环境(例如浏览器),允许它们使用状态、效果和事件侦听器来处理交互性,还可以访问浏览器专有的 API(例如地理定位或 localStorage),从而允许你构建特定用途的前端正如我们在引入 RSC 架构之前这些年所做的那样。

事实上,术语客户端组件并不意味着任何新东西;而是意味着什么。它只是有助于将这些组件与新引入的服务器组件区分开来。

以下是 Counter 客户端组件的示例:

"use client"

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>Counter</h2>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

服务器组件

服务器组件代表了一种新型的 React 组件,专门设计用于专门在服务器上运行。与客户端组件不同,它们的代码保留在服务器上,永远不会下载到客户端。这种设计选择为 React 应用程序提供了多种好处。让我们仔细看看这些好处。

Zero-bundle 大小

首先,就捆绑包大小而言,服务器组件不会将代码发送到客户端,从而允许大量依赖项保留在服务器端。通过消除为这些组件下载、解析和执行 JavaScript 的需要,这对互联网连接速度较慢或设备功能较差的用户有利。此外,它还消除了水合步骤,从而加快了应用程序的加载和交互速度。

直接访问服务器端资源

其次,通过直接后端访问服务器端资源(例如数据库或文件系统),服务器组件可以实现高效的数据获取和呈现,而无需额外的客户端处理。利用服务器的计算能力和与数据源的邻近性,它们管理计算密集型渲染任务,并仅向客户端发送交互式代码片段。

增强安全性

第三,服务器组件的专有服务器端执行通过将敏感数据和逻辑(包括令牌和 API 密钥)远离客户端来增强安全性。

改进数据获取方式

第四,服务器组件提高了数据获取效率。通常,当使用 useEffect 在客户端获取数据时,子组件在父组件完成加载自己的数据之前无法开始加载其数据。这种顺序获取数据通常会导致性能不佳。

主要问题不是往返本身,而是这些往返是从客户端到服务器的。服务器组件使应用程序能够将这些顺序往返转移到服务器端。通过将此逻辑移至服务器,可以减少请求延迟,提高整体性能,从而消除客户端-服务器瀑布。

缓存

第五,在服务器上渲染可以缓存结果,可以在后续请求中以及跨不同用户重用。这种方法可以通过最大限度地减少每个请求所需的渲染和数据获取量来显着提高性能并降低成本。

更快的初始页面加载和首次内容绘制

第六,服务器组件显着改进了初始页面加载和首次内容绘制 (FCP)。通过在服务器上生成 HTML,页面立即呈现,不会延迟下载、解析和执行 JavaScript。

改进SEO

第七,关于搜索引擎优化 (SEO),服务器渲染的 HTML 完全可供搜索引擎机器人访问,从而增强页面的可索引性。

高效streaming

最后,还有流媒体。服务器组件允许将渲染过程划分为可管理的块,然后在准备好后立即将其传输到客户端。这种方法允许用户更早地开始查看页面的某些部分,而无需等待整个页面在服务器上完成呈现。

以下是 ProductList 页面服务器组件的示例:

解释export default async function ProductList() {
  const res = await fetch("https://api.example.com/products");
  const products = res.json();

  return (
    <main>
      <h1>Products</h1>
      {products.length > 0 ? (
        <ul>
          {products.map((product) => (
            <li key={product.id}>
              {product.name} - ${product.price}
            </li>
          ))}
        </ul>
      ) : (
        <p>No products found.</p>
      )}
    </main>
  );
}

“use client”指令

在 React 服务器组件范例中,在默认情况下,Next.js 应用程序中的每个组件都被视为服务器组件。

要定义客户端组件,我们必须在文件顶部包含一条指令(换句话说,一条特殊指令):"use client"。该指令充当我们跨越从服务器到客户端边界的门票,并且允许我们定义客户端组件。

它向捆绑器发出信号,表明该组件及其导入的任何组件均用于客户端执行。因此,该组件可以获得对浏览器 API 的完全访问权限以及处理交互性的能力。

“use server”指令标记可以从客户端代码调用的服务器端函数。我们将在单独的帖子中介绍“使用服务器”和服务器操作。

服务器组件渲染生命周期

让我们假设 Next.js 作为 React 框架来探索 RSC 渲染生命周期。

带有 Next.js 13 的 Vercel 是第一个支持 React 服务器组件 (RSC) 架构的。

对于 React 服务器组件 (RSC),重要的是要考虑三个元素:浏览器(客户端)以及服务器端的 Next.js(框架)和 React(库)。

初始加载顺序

7

  • 当你的浏览器请求页面时,Next.js应用程序路由器会将请求的 URL 与服务器组件匹配。然后,Next.js指示 React 渲染该服务器组件。
  • React 渲染服务器组件和任何也是服务器组件的子组件,将它们转换为称为 RSC 有效负载的特殊 JSON 格式。如果任何服务器组件挂起,React 会暂停该子树的渲染,并发送一个占位符值。
  • 同时,客户端组件会准备好生命周期后期的指令。
  • Next.js 使用 RSC Payload 和客户端组件 JavaScript 指令在服务器上生成 HTML。此 HTML 将流式传输到你的浏览器,以立即显示路线的快速、非交互式预览。
  • 此外,Next.js 在 React 渲染每个 UI 单元时,以流式传输 RSC 有效负载。
  • 在浏览器中,Next.js处理流式的 React 响应。React 使用 RSC 有效负载和客户端组件指令来逐步渲染 UI。
  • 加载所有客户端组件和服务器组件的输出后,将向用户显示最终的 UI 状态。
  • 客户端组件经过水合处理,将我们的应用程序从静态显示转变为交互式体验。

这是初始加载的顺序,接下来,让我们看一下刷新应用程序部分的更新顺序。

更新顺序

8

  • 浏览器请求重新获取特定 UI,例如完整路由。
  • Next.js处理请求并将其与请求的服务器组件匹配。Next.js指示 React 渲染组件树。React 渲染组件,类似于初始加载。
  • 但是,与初始序列不同的是,没有用于更新的 HTML 生成。Next.js逐步将响应数据流式传输回客户端。
  • 收到流式响应后,Next.js 会使用新输出触发路由的重新呈现。
  • React 将新渲染的输出与屏幕上的现有组件协调(合并)。由于 UI 描述是一种特殊的 JSON 格式而不是 HTML,因此 React 可以更新 DOM,同时保留关键的 UI 状态,例如焦点或输入值。

以上就是 Next.js 中 App Router 的 RSC 渲染生命周期的本质。

在 React 服务器组件架构中,服务器组件负责数据获取和静态渲染,而客户端组件的任务是渲染应用程序的交互元素。

最重要的是,RSC 架构使 React 应用程序能够利用服务器和客户端渲染的最佳方面,同时使用单一语言、单一框架和一组有凝聚力的 API。RSC 改进了传统的渲染技术,同时也克服了它们的局限性。

有关 RSC 的更多背景和更全面的心智模型,请参阅Next.js文档或在 YouTube 上观看我的Next.js教程