为什么 RSC 是正确答案?


在过去的十年里,React及其生态系统不断演进。每个版本都引入了新的概念、优化,有时甚至是范式转变,推动了我们对Web开发可能性的认知边界。

React Server Components (RSC)是自React hooks以来最新、也许是最重要的变化。然而,这一变化在社区内引起了不同的反应。

对我来说,Linkin Park的这句歌词表达了我们步入2024年时围绕React演进的情感:
因为一旦你有了一个关于事物如何运作的理论,每个人都希望下一个东西和第一个一样
我们已经习惯了我们所熟悉和热爱的`React,因此理解一个范式转变,可以理解地带有犹豫和怀疑的挑战。

本文的目标是引导您穿越多年来React渲染演进的旅程,并帮助您理解为什么React Server Components 不仅是不可避免的,而且是构建成本效益高、性能优异的React应用程序的未来,从而提供出色的用户体验。

Client-side rendering (CSR)

如果您已经从事开发工作一段时间,您会记得React是创建单页面应用程序(SPA)的首选库。

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

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

当您在DOM Inspector中看到HTML出现时,但在查看源代码选项中看不到时,就能看到这个过程。

在这种渲染方式中,组件代码在浏览器(客户端)内部直接转换为用户界面,这被称为Client-side rendering (CSR)

这是一个React SPADOM检查器与页面源代码的对比:
image.png

CSR DOM vs Source

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

CSR的弊端

首先,生成主要包含单个div标记的HTML并不是SEO的最佳选择,因为它为搜索引擎提供的内容很少。大的包大小和来自深度嵌套组件的API响应的网络请求瀑布可能导致有意义的内容呈现速度不够快,无法让爬虫对其进行索引。
其次,让浏览器(客户机)处理所有的工作,例如获取数据、计算UI和使HTML具有交互性,可能会减慢速度。在页面加载时,用户可能会看到一个空白屏幕或加载旋转器。随着时间的推移,这个问题趋于恶化,因为添加到应用程序中的每个新特性都会增加JavaScript包的大小,从而延长用户看到UI的等待时间。这种延迟对于网速较慢的用户来说尤其明显。
CSR为我们今天使用的交互式web应用奠定了基础,但为了增强SEO和性能,开发人员开始寻找更好的解决方案。

Server-side Rendering (SSR)

为了克服CSR的缺点,像Next.js这样的现代React框架转向了服务器端解决方案。这种方法从根本上改变了内容传递给用户的方式。
服务器不再发送依赖于客户端JavaScript构建页面的几乎为空的HTML文件,而是负责呈现完整的HTML。然后将这个完全格式化的HTML文档直接发送到浏览器。由于HTML是在服务器上生成的,因此浏览器能够快速解析并显示它,从而缩短了初始页面加载时间。

解决CSR的弊端

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

  • 首先,它显著地改进了SEO,因为搜索引擎可以很容易地为服务器呈现的内容建立索引。
  • 第二,浏览器可以立即加载页面的HTML内容,而不是一个空白的屏幕或加载微调器。
    Hydration
    SSR的方法立即改善了内容的可见性,但在页面的交互性方面有其自身的复杂性。页面的全部交互性被搁置,直到浏览器完全下载并执行了JavaScript捆绑包——其中包括React本身以及您的应用程序特定的代码。

这个重要阶段,称为Hydration,是由服务器最初提供的静态页面实现的。在Hydration过程中,React在浏览器中接管,根据提供的静态HTML重建组件树。它精心规划在这棵树中的交互元素的位置。

然后,React开始将必要的JavaScript逻辑绑定到这些元素上。这涉及初始化应用程序状态,为诸如点击和悬停等操作附加事件处理程序,并设置任何其他用于完全交互式用户体验的动态功能。

SSG和SSR

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

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

另一方面,SSR根据用户请求即时渲染页面。它适用于个性化内容,比如社交媒体动态,其中HTML取决于登录用户。通常,您会看到这两种方法被统称为服务器端渲染或SSR。

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

SSR的缺点

SSR的一个问题是组件不能开始渲染,然后暂停或“等待”,而数据仍在加载中。如果一个组件需要从数据库或另一个来源(如API)获取数据,那么在服务器开始渲染页面之前,这个获取过程必须完成。这可能会延迟服务器对浏览器的响应时间,因为服务器必须在向客户端发送页面的任何部分之前收集所有必要的数据。

SSR的第二个问题是,为了成功地进行Hydration,即React为服务器渲染的HTML添加交互性,浏览器中的组件树必须与服务器生成的组件树完全匹配。这意味着在您开始对任何组件进行水合之前,必须在客户端加载所有组件的JavaScript

SSR的第三个问题与Hydration本身有关。React在单一步骤中对组件树进行Hydration,这意味着一旦开始Hydration,它就不会停止,直到完成整个树的Hydration。因此,在您可以与任何组件进行交互之前,所有组件都必须进行Hydration

这三个问题——必须加载整个页面的数据,加载整个页面的JavaScript,以及Hydration整个页面——创建了一个从服务器到客户端的全方位瀑布问题,其中每个问题都必须在进入下一个问题之前解决。如果您的应用程序的某些部分比其他部分慢,这种方法就不太高效,而这在实际应用程序中经常发生。
由于这些限制,React团队引入了一种新的、改进的SSR架构。

服务器端渲染的Suspense

React 18引入了服务器端渲染的悬挂以解决传统SSR的性能缺陷。这种新架构允许您使用<Suspense>组件来解锁两个主要的SSR功能:

  • 在服务器上进行HTML流式处理
  • 在客户端进行选择性Hydration
    服务器上的HTML流式处理
    正如我们在上一节中讨论的,传统上,SSR是一个全盘接受的事务。服务器渲染完整的HTML,然后将其发送到客户端。客户端显示此HTML,仅在完整的JavaScript捆绑包加载后,React才继续对整个应用程序进行水合,以添加交互性。


    然而,有了React 18,我们有了一个新的可能性。通过将页面的一部分,例如主要内容区域,包裹在React Suspense 组件内,我们告诉React它不需要等待主节数据被获取,才能开始流式处理页面的其余部分的HTMLReact将发送一个占位符,如加载中的加载图标,而不是完整的内容。

一旦服务器准备好了主节数据,React会通过持续的流发送附加的HTML,伴随着一个内联的<script>标签,其中包含正确定位该HTML所需的最小JavaScript。由于这个,即使在客户端完整加载React库之前,主节的HTML也对用户可见。

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

在客户端进行选择性Hydration

虽然我们现在可以加速初始HTML的交付,但我们仍然面临另一个挑战。在加载主节的JavaScript之前,客户端应用程序Hydration不能开始。如果主项目的JavaScript捆绑包很大,这可能会显著延迟进程。

为了缓解这个问题,可以使用代码分割。代码分割意味着您可以将特定的代码段标记为不立即需要加载,向您的构建平台将它们分成单独的<script>标签。

使用React.lazy进行代码分割使您能够将主项目的代码与主要JavaScript捆绑包分离开来。因此,包含React和整个应用程序代码的JavaScript现在可以由客户端独立下载,而不必等待主节的代码。

这很重要,因为通过将主项目包装在<Suspense>中,您告诉React它不应该阻止页面的其余部分仅仅是流式处理,而是还应该进行Hydration。这个功能,称为选择性水合,允许在完整的HTMLJavaScript代码完全下载之前,根据可用性对部分进行水合。

从用户的角度来看,最初他们获得的是流式传输的非交互式内容。然后您告诉React进行Hydration。主项目的JavaScript代码还没有加载,但这没关系,因为我们可以选择性地对其他组件进行水合。

一旦加载了其代码,主项目就会进行Hydration

由于选择性Hydration,一个大块的JS不会阻止页面的其余部分变得交互。


此外,选择性Hydration提供了解决第三个问题的解决方案:“为了与任何组件进行交互,必须将所有内容进行Hydration”。React尽快开始Hydration,使得可以与诸如页眉和侧导航等元素进行交互,而无需等待主要内容被Hydration。这个过程由React自动管理。

在需要等待Hydration的多个组件的情况下,React根据用户交互优先进行Hydration。例如,如果侧边栏即将进行Hydration,而您点击了主内容区域,React将在点击事件的捕获阶段同步进行Hydration,以确保组件立即准备好响应用户交互。侧边栏稍后进行Hydration

基于用户交互的Hydration的流程如下:

Suspense 缺点
  • 首先,即使JavaScript代码以异步方式流式传输到浏览器,最终,用户仍然必须下载整个网页的所有代码。随着应用程序添加更多功能,用户需要下载的代码量也在增长。这引出了一个重要的问题:用户是否真的需要下载这么多数据?

  • 其次,当前的方法要求所有React组件在客户端进行Hydration,而不管它们实际是否需要交互性。这个过程可能会浪费资源,并延长用户的加载时间和交互时间,因为他们的设备需要处理和渲染甚至可能不需要客户端交互的组件。这引出了另一个问题:是否应该对所有组件进行Hydration,甚至那些不需要交互性的组件?

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

为了解决这些挑战,简单地迈出一小步是不够的。我们需要朝着更强大的解决方案迈出重要的一步。

React Server Components (RSC)

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

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

Client components

Client components是我们在以前的渲染技术中已经使用和讨论过的熟悉的React组件。它们通常在client-side (CSR) 上渲染,但它们也可以一次在服务器上(SSR)渲染到HTML,让用户立即看到页面的HTML内容,而不是空白屏幕。

Client components在服务器上渲染的概念可能看起来令人困惑,但将它们视为主要在客户端运行但也可以(并且应该)作为优化策略之一在服务器上执行一次是很有帮助的。

Client components可以访问客户端环境,例如浏览器,使其能够使用状态、效果和事件侦听器来处理交互,并且还可以访问浏览器专有的API,如地理位置或localStorage,使您能够为特定用例构建前端,就像在引入RSC架构之前多年一样。

实际上,Client components这个术语并不表示任何新内容;它只是帮助将这些组件与新引入的服务器组件区分开来。

这是一个Counter客户端组件的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
"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>
);
}
Server components

Server components代表一种新类型的React组件,专门设计用于仅在服务器上运行。与客户端组件不同,它们的代码保留在服务器上,从不下载到客户端。这种设计选择为React应用程序带来了多个好处。让我们更仔细地看看这些好处。

  • Zero-bundle
    首先,在bundle大小方面,服务器组件不会向客户端发送代码,允许大型依赖保持在服务器端。这通过消除了需要为这些组件下载、解析和执行JavaScript的用户来受益,从而使用户能够更快地加载和交互,同时也消除了hydration步骤。

  • 直接访问服务器端资源
    第二,通过直接访问后端的服务器端资源,如数据库或文件系统,服务器组件实现了高效的数据获取和渲染,而不需要额外的客户端处理。利用服务器的计算能力和与数据源的接近性,它们管理计算密集型的渲染任务,并仅将交互式代码片段发送到客户端。

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

-改进的数据获取
第四,服务器组件增强了数据获取的效率。通常,在客户端使用useEffect获取数据时,子组件在父组件完成加载数据之前不能开始加载其自己的数据。这种顺序获取数据往往导致性能不佳
主要问题不在于往返行程本身,而在于这些往返行程是从客户端到服务器端的。服务器组件使应用程序能够将这些顺序往返行程移至服务器端。通过将此逻辑移到服务器上,请求延迟减少,总体性能提高,消除了客户端-服务器的瀑布流。

  • 缓存
    第五,服务器端渲染使结果能够进行缓存,在后续请求和不同用户之间可以重用。这种方法可以通过减少每个请求所需的渲染和数据获取的数量,显着提高性能并降低成本。

  • 更快的初始页面加载和第一个内容绘制
    第六,通过在服务器上生成HTML,服务器组件显着改善了初始页面加载和首屏渲染(FCP)。页面在没有下载、解析和执行JavaScript的延迟的情况下立即渲染。

  • 改进的SEO
    第七,关于搜索引擎优化(SEO),服务器端渲染的HTML对搜索引擎机器人完全可见,增强了页面的可索引性。

  • 高效处理
    最后,还有流式处理。服务器组件允许将渲染过程分成可管理的块,然后在准备好时将它们作为流式传输到客户端。这种方法允许用户更早地开始看到页面的部分内容,消除了在服务器上完成整个页面的渲染的需要等待的必要性。
    这是一个ProductList页面服务器组件的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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” directive
    React服务器组件范式中,重要的是要注意,默认情况下,Next.js应用程序中的每个组件都被视为服务器组件。
    要定义客户端组件,我们必须在文件顶部包含一个指令——换句话说,一个特殊的指示——“使用客户端”。这个指令就像我们从服务器端到客户端的边界的通行证,它是允许我们定义客户端组件的关键。
    它向bundler发出信号,表明此组件以及它导入的任何组件都是用于客户端执行的。结果,该组件可以完全访问浏览器API,并具有处理交互性的能力。

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

React服务器组件渲染生命周期

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

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

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

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

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

更新序列

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

这就是Next.js中应用程序路由器的RSC渲染生命周期的本质。

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

总之,RSC架构使React应用程序能够充分利用服务器和客户端渲染的最佳方面,同时使用单一语言、单一框架和一致的API集。RSC改进了传统渲染技术,同时也克服了它们的局限性。

原文:https://www.builder.io/blog/why-react-server-components#html-streaming-on-the-server