为什么需要服务端组件RSC

33 分钟
google/gemini-2.5-pro-exp-03-25 Logo

AI 摘要 (由 google/gemini-2.5-pro-exp-03-25 生成)

本文阐述了Web渲染从MPA到SPA、SSR的演进及痛点。React服务端组件(RSC)通过仅在服务器执行且代码零客户端体积,解决了JS负担过重问题,提升性能与数据获取效率。RSC与客户端组件协作,需框架支持,但也引入了新复杂性。

28.21s
~22449 tokens

1. 前言:从 MPA 到 SPA 与 SSR

在 React 出现之前,传统的 Web 开发通常使用 ASP、PHP 等技术构建多页面应用 (MPA)。这种架构下,服务器负责访问数据库、渲染模板,然后将生成的完整 HTML 页面发送给客户端浏览器。然而,这种模式存在明显的弊端:  

  • 服务器负载高,响应时间长:每次用户请求新页面或进行交互时,服务器都需要重新获取数据并完整渲染 HTML,这不仅增加了服务器的计算压力,也延长了用户的等待时间,影响体验。  
  • 前后端耦合紧密,维护困难:UI 逻辑和数据处理逻辑混合在服务器端,导致前后端职责不清,代码耦合度高,维护和迭代变得困难。  
  • 用户体验不佳:页面跳转时的白屏、缺乏流畅的交互和视觉连续性,使得 MPA 在用户体验上往往不如现代应用

因此,现代Web开发越来越倾向于使用React等前端框架来构建更动态、响应迅速的SPA。这方式不仅减轻了服务器的压力,也提升了应用的可维护性和体验。

为了克服这些问题,单页面应用 (SPA) 应运而生,以 React、Vue、Angular 等为代表的前端框架极大地推动了 SPA 的发展。SPA 将大部分渲染逻辑移到客户端(浏览器)执行,通过 JavaScript 动态更新页面内容,提供了接近原生应用的流畅交互体验。  

然而,纯粹的客户端渲染 (CSR) 模式也带来了新的挑战:

  • 初始加载缓慢:浏览器首次加载时只收到一个空的 HTML 骨架和大量的 JavaScript 文件。在 JS 下载、解析和执行完成之前,用户看到的是白屏,这导致较慢的首次内容绘制 (FCP - First Contentful Paint)。  
  • SEO 不友好:搜索引擎爬虫可能无法有效执行 JavaScript,导致难以抓取和索引由 JS 动态生成的内容。  

为了解决 SPA 的这些痛点,服务端渲染 (SSR) 技术被重新引入到现代前端框架中。SSR 的核心思想是在服务器端预先执行 React 组件代码,生成包含内容的初始 HTML,直接发送给浏览器。这样用户可以更快地看到页面内容(改善 FCP),同时也方便了搜索引擎爬虫抓取。  

但 SSR 并非终点。它本身是为了弥补纯客户端渲染缺陷而采取的一种策略,其自身的局限性也直接推动了 Web 渲染技术的进一步演进,最终导向了 React Suspense 和服务端组件 (RSC) 的诞生。可以说,Web 开发的演进过程,就是在不断寻求平衡服务器能力与客户端体验、追求更优性能和开发效率的过程。  

2. 传统 SSR 的核心问题与局限性

传统的 SSR 模式虽然改善了初始加载速度和 SEO 问题,但其工作机制也带来了新的挑战,主要体现在所谓的“一切或全无”瀑布流问题和沉重的客户端负担上。  

传统ssr

“一切或全无”瀑布流 (The "All-or-Nothing" Waterfall)

  1. 数据获取阻塞 (Data Fetching Blocking):在服务器端,必须等待页面所需的所有数据都获取完毕后,才能开始生成 HTML。如果其中任何一个数据请求缓慢,整个页面的响应都会被延迟。这就像我之前有个宿迁的大学同学,每次和他出去吃饭的时候非要等到菜全部上齐他才允许大家动筷子,好无奈,等的时间也很煎熬。
  2. JavaScript 下载阻塞 (JavaScript Download Blocking):浏览器收到服务器渲染的 HTML 后,并不能立即与之交互。它必须等待页面所需的所有 JavaScript 代码(包括 React 运行时、所有组件代码、第三方库等)全部下载完成,才能开始执行下一步的“水合”(Hydration) 过程。  
  3. 水合过程阻塞 (Hydration Blocking):水合是指在客户端执行 JavaScript,将服务器发送的静态 HTML 与组件逻辑关联起来,并附加事件监听器,使其变为完全可交互的 React 应用的过程。在传统 SSR 中,React 需要一次性水合整个应用的组件树。这意味着在所有组件完成水合之前,用户无法与页面上的任何元素进行交互。  

沉重的客户端负担 (Client-Side Burden)

  1. 庞大的 JavaScript 包 (Large Bundles):SSR 并没有减少最终需要发送到客户端的 JavaScript 总量。所有在页面上使用的组件代码,无论其是否需要交互,最终都会被打包发送到浏览器。随着应用功能的增加,这个包会越来越大。  
  2. 高昂的水合成本 (Hydration Cost):水合过程并非没有代价。它需要在客户端重新执行组件的渲染逻辑,构建虚拟 DOM,并将其与服务器生成的 HTML 进行比对和关联,这会消耗客户端的 CPU 资源。  
  3. 设备性能影响 (Device Performance Impact):大量的 JavaScript 下载、解析、执行以及水合过程,对用户的设备性能(尤其是低端移动设备)和网络状况提出了较高要求,可能导致页面实际可交互的时间 (TTI - Time To Interactive) 延迟,甚至出现卡顿。  

因此,传统 SSR 虽然解决了 FCP 问题,让用户更快地“看到”内容,但往往以牺牲 TTI 为代价。由于需要下载和执行庞大的 JavaScript 并完成整个应用的水合,用户可能需要等待更长时间才能真正“使用”页面,这与精心优化的、采用代码分割的 CSR 应用相比,实际可交互时间可能反而更长。此外,开发者还需要维护服务器端渲染和客户端运行两个环境,处理可能的状态同步问题,增加了开发的复杂性。这些局限性促使 React 团队寻求更优化的解决方案。  

3. React 18 Suspense 对 SSR 的改进

React v18.0 – React
The library for web and native user interfaces
网站图标react.dev
预览图片

为了缓解传统 SSR 的瀑布流问题,React 18 引入了基于 Suspense 的全新 SSR 架构,带来了两大关键改进:HTML 流式传输和选择性水合。  

HTML 流式传输 (Streaming HTML)

通过将页面中可能加载较慢的部分(例如需要异步获取数据的组件)用 <Suspense> 组件包裹起来,并提供一个 fallback UI(如加载指示器),React 服务器渲染器不再需要等待所有数据都准备好才开始发送 HTML。它可以先发送页面的整体骨架和非 Suspense 包裹部分的内容,对于 Suspense 包裹的部分,则先发送 fallback UI 的 HTML。当服务器端的数据准备就绪后,React 会将该组件渲染成 HTML,并通过同一个流式连接将这段 HTML 追加发送给客户端。客户端 JavaScript 接收到这段新的 HTML 后,会将其“填入”到对应的位置。  

这种机制打破了 SSR 必须等待所有数据就绪才能响应的限制,让用户能够更快地看到页面的部分内容,改善了 FCP。

选择性水合 (Selective Hydration)

仅仅流式传输 HTML 还不够,因为页面的交互性仍然依赖于 JavaScript 的下载和执行。React 18 的选择性水合允许客户端在接收到流式 HTML 的同时,就开始进行水合操作,并且可以根据用户的交互行为来确定水合的优先级。  

例如,在一个电商页面中,即使产品推荐部分的数据还在加载(服务器还在流式传输其 HTML),如果用户点击了已经渲染好的导航栏或者产品列表中的某个按钮,React 会优先下载并执行与被交互组件相关的 JavaScript 代码,并对其进行水合,使其能够响应用户的操作。其他非关键部分的水合则可以稍后进行。这避免了传统 SSR 中必须等待所有 JS 下载完毕、整个应用水合完成后才能交互的尴尬局面,显著改善了 TTI。  

举例说明:

假设一个页面包含导航栏 (NavBar)、产品列表 (ProductList) 和推荐栏 (Recommendations),使用 Suspenselazy 实现:

typescript
import React, { Suspense, lazy } from 'react';

// 使用 lazy 动态导入组件
const NavBar = lazy(() => import('./NavBar'));
const ProductList = lazy(() => import('./ProductList'));
const Recommendations = lazy(() => import('./Recommendations'));

function HomePage() {
  return (
    <div>
      {/* NavBar 可能很快加载 */}
      <Suspense fallback={<div>Loading NavBar...</div>}>
        <NavBar />
      </Suspense>
      {/* ProductList 可能需要一些时间获取数据 */}
      <Suspense fallback={<div>Loading Product List...</div>}>
        <ProductList />
      </Suspense>
      {/* Recommendations 可能加载最慢 */}
      <Suspense fallback={<div>Loading Recommendations...</div>}>
        <Recommendations />
      </Suspense>
    </div>
  );
}

export default HomePage;

工作流程:

  1. 服务器开始渲染 HomePage。遇到 NavBarSuspense,如果 NavBar 组件代码或数据未就绪,则发送 Loading NavBar... 的 HTML。
  2. 继续渲染,遇到 ProductListSuspense,发送 Loading Product List... 的 HTML。
  3. 遇到 RecommendationsSuspense,发送 Loading Recommendations... 的 HTML。
  4. 客户端收到初始 HTML 并开始显示 fallback 内容。同时开始下载 JavaScript。
  5. 假设 NavBar 的数据和代码先就绪,服务器将 NavBar 的 HTML 流式传输到客户端,客户端 JS 接收到后替换 fallback 并开始水合 NavBar
  6. ProductList 接着就绪,其 HTML 被流式传输并水合。
  7. 如果此时用户点击了 NavBarProductList 中的元素,React 会优先完成这些组件的水合以响应交互。
  8. 最后 Recommendations 的 HTML 到达并被水合。

剩余的局限性

尽管 Suspense 显著优化了 SSR 的用户体验,但它并没有改变一个根本事实:所有需要在客户端交互的组件,其 JavaScript 代码最终仍然需要被下载到浏览器并执行水合。Suspense 优化的是代码和数据的传输与执行时机,而不是传输与执行的总量。随着应用规模的增长,客户端需要下载和处理的 JavaScript 总量依然会增加,客户端的计算负担问题并未得到根本解决。正是这一核心局限性,成为了 React 服务端组件 (RSC) 诞生的最主要驱动力。  

4. 引入 React 服务端组件 (RSC) - 核心概念

为了从根本上解决客户端 JavaScript 负担过重的问题,React 团队引入了一种全新的组件类型——React 服务端组件 (React Server Components, RSC)。这不仅仅是对现有 SSR 的优化,而是一次深刻的范式转变,旨在更智能地划分服务器和客户端的职责。  

定义服务端组件 (RSC)

服务端组件是一种特殊的 React 组件,具有以下核心特征:

  • 仅在服务器端运行:RSC 的代码只在服务器环境(可以在构建时运行,也可以在每次请求时运行)执行,用于生成 UI 描述。  
  • 提前渲染 (Render Ahead of Time):RSC 的渲染发生在打包和传统 SSR 步骤之前,在一个独立于客户端应用或 SSR 服务器的环境中进行。  
  • 零打包体积影响 (Zero Bundle Size Impact):RSC 组件本身的代码,以及它们使用的依赖库,永远不会被包含在发送到客户端的 JavaScript 包中。这是 RSC 最核心的优势之一。  
  • 渲染为中间格式 (Render to Intermediate Format):RSC 并不直接渲染为 HTML 字符串。它们渲染为一种特殊的、可序列化的中间数据格式(在 Next.js 中称为 RSC Payload),这种格式描述了渲染出的 UI 结构,可以被流式传输到客户端。  
  • 限制 (Limitations):由于不在浏览器中运行,RSC 不能使用客户端特有的 React Hooks(如 useState, useEffect)或访问浏览器 API(如 window, document)。它们是无状态、无副作用的(相对于浏览器环境而言)。  
  • 异步能力 (Async Components):RSC 可以是 async 函数,允许直接在组件内部使用 await 来进行数据获取或其他异步操作,简化了服务端的数据处理逻辑。  

定义客户端组件 (Client Components, CC)

与 RSC 相对的是客户端组件,它们是我们一直以来熟悉的标准 React 组件:

  • 即“传统”React 组件:它们就是我们过去编写的、用于构建用户界面的 React 组件。  
  • "use client" 指令:为了区分 RSC 和 CC,客户端组件需要在文件顶部明确声明 "use client" 指令。  
  • 双端渲染 (Render on Both Server and Client):客户端组件的代码会在服务器端运行(用于生成初始 HTML 的 SSR 过程),并且也会被发送到客户端,在浏览器中再次运行(用于水合和后续的交互)。  
  • 包含在客户端包中 (Included in Client Bundle):客户端组件的代码及其依赖项被包含在发送给浏览器的 JavaScript 包中。  
  • 完全交互能力 (Full Interactivity):客户端组件可以使用 React 的所有功能,包括 useState, useEffect, useContext 等 Hooks,可以访问浏览器 API,处理用户事件,管理状态和副作用。  

RSC 与 SSR 的关键区别

理解 RSC 和 SSR 的关系至关重要:

  • RSC 不是 SSR 的替代品,而是补充:RSC 和 SSR 是协同工作的。RSC 负责渲染那些不需要客户端交互的部分,并生成中间载荷;而 SSR(特指对客户端组件的 SSR)则利用这个载荷以及客户端组件的代码,生成最终的初始 HTML。  
  • 代码传输差异:RSC 的代码发送到客户端;SSR(针对客户端组件)的组件代码发送到客户端用于水合。  
  • 渲染目标差异:RSC 渲染为 RSC Payload;SSR 渲染为 HTML。  

共享组件 (Shared Components)

还存在一类组件,它们既不包含仅能在服务器端运行的逻辑(如直接访问数据库),也不包含仅能在客户端运行的逻辑(如 useState)。这类组件被称为共享组件,它们可以在 RSC 或 CC 中使用。当被 RSC 导入时,它们作为 RSC 运行;当被 CC 导入时,它们作为 CC 运行,其代码会被包含在客户端包中。  

渲染模式对比

为了更清晰地理解这几种模式的区别,下表进行了总结:

特性 (Feature)

客户端渲染 (CSR)

传统服务端渲染 (SSR)

React 服务端组件 (RSC) + SSR for CC

初始 HTML (Initial HTML)

空白或骨架 (Empty/Shell)

完整静态内容 (Full Static)

完整静态内容 (Full Static)

主要渲染地点 (Primary Rendering)

客户端 (Client)

服务器 (初始) + 客户端 (水合/更新) (Server (initial) + Client (hydration/updates))

服务器 (RSC) + 服务器/客户端 (CC) (Server (RSC) + Server/Client (CC))

JavaScript 包体积 (JS Bundle Size)

大,随应用增长 (Large, grows with app)

大,随应用增长 (Large, grows with app)

更小,RSC 代码不包含 (Smaller, RSC code excluded)

客户端计算负担 (Client Load)

高 (High)

高 (水合) (High (Hydration))

更低 (无 RSC 水合) (Lower (No RSC hydration))

数据获取 (Data Fetching)

客户端瀑布流 (Client-side waterfalls)

服务器端 (初始) 阻塞 / 客户端瀑布流 (Server-side (initial) blocking / Client-side waterfalls)

服务器端直接访问 (RSC) / 客户端 (CC) (Server-side direct access (RSC) / Client-side (CC))

首次内容绘制 (FCP)

慢 (Slow)

快 (Fast)

快 (Fast)

可交互时间 (TTI)

取决于 JS 大小/网络 (Depends on JS size/network)

可能较慢 (JS 下载 + 水合) (Potentially slow (JS download + hydration))

可能更快 (更少 JS, 无 RSC 水合) (Potentially faster (less JS, no RSC hydration))

SEO

差 (需预渲染) (Poor without pre-rendering)

好 (Good)

好 (Good)

交互性 (Interactivity)

完全 (Full)

完全 (水合后) (Full (after hydration))

完全 (客户端组件) (Full (Client Components))

复杂性 (Complexity)

初始设置较低 (Lower initial setup)

更高 (服务器设置, 水合) (Higher (server setup, hydration))

更高 (新概念, 边界, 框架) (Higher (new concepts, boundaries, framework))

RSC 的本质:预渲染而非直接渲染

需要强调的是,RSC 的渲染过程与传统的 SSR 或模板引擎直接输出 HTML 不同。RSC 渲染产生的是一种结构化的中间数据(RSC Payload),它描述了 UI 的状态和结构,包括渲染好的 RSC 部分、客户端组件的占位符及其所需的 JS 引用、以及传递给客户端组件的 props。这个 Payload 随后被用于流式传输,并指导客户端 React 进行高效的 DOM 更新和客户端组件的水合。这种“预渲染到载荷”的机制是 RSC 能够实现零打包体积、与 Suspense 深度集成等优势的基础。  


默认行为的重要性:思维转变

在像 Next.js App Router 这样的现代框架中,组件默认被视为服务端组件。开发者必须显式地使用 "use client" 指令来标记那些需要在客户端运行并具有交互性的组件。这不仅仅是一个技术细节,它代表了一种重要的思维转变:从过去默认一切在客户端运行(除非明确进行 SSR),转变为现在默认一切在服务器端运行(除非明确需要客户端交互)。这种“服务器优先”的理念促使开发者更审慎地思考哪些部分真正需要客户端 JavaScript,从而有助于构建更轻量、更高效的应用。  

Making Sense of React Server Components • Josh W. Comeau
This year, the React team unveiled something they've been quietly researching for years: an official way to run React components exclusively on the server. This is a significant paradigm shift, and it's caused a whole lot of confusion in the React community. In this tutorial, we'll explore this new world, and build an intuition for how it works, and how we can take advantage of it.
网站图标www.joshwcomeau.com
预览图片

5. 为什么需要 RSC?解决核心痛点 (Benefits Focus)

引入 RSC 的核心动机是为了解决传统 Web 应用架构(包括 CSR 和 SSR)中存在的关键痛点。RSC 通过其独特机制,在多个方面带来了显著的优势:

零打包体积影响 (Zero Bundle Size Impact)

这是 RSC 最具革命性的特点。

  • RSC 代码不发送至客户端:如前所述,服务端组件的代码仅在服务器上执行,不会被打包进发送到浏览器的 JavaScript 文件中。  
  • 服务端依赖隔离:更重要的是,那些被 RSC 使用的第三方库或依赖项,也同样不会被发送到客户端。  
  • 实例:移除大型库:考虑一个场景,你需要渲染一段 Markdown 内容。在传统 React 应用中,即使内容是静态的,你也需要在客户端引入 markedsanitize-html 这样的库来处理渲染,这可能增加几十 KB (gzipped) 的包体积。使用 RSC,你可以在服务端组件中引入并使用这些库来生成 HTML,而这些库的代码完全不会影响客户端包的大小。  
  • 收益:这极大地减小了客户端需要下载、解析和执行的 JavaScript 体积,从而加快了页面加载速度,降低了客户端的运行负担,对于网络环境较差或设备性能较低的用户尤其友好。  

这种零打包体积影响的特性,从根本上改变了开发者对第三方库选择的考量。过去因为担心增加客户端负担而避免使用的一些功能强大但体积庞大的库(如复杂的日期处理库、数据可视化库、重量级 UI 库的非交互部分),现在如果只在 RSC 中使用,就变得完全可行。这为在服务器端构建更丰富、更强大的非交互式 UI 体验打开了大门,而无需牺牲客户端性能。

高效数据获取 (Efficient Data Fetching)

RSC 改变了 React 应用中数据获取的方式:

  • 直接访问后端资源:由于 RSC 在服务器上运行,它们可以安全、直接地访问后端数据源,如数据库、文件系统、内部 API 或微服务,无需像客户端那样必须通过公开的 API 端点进行通信。  
  • 组件内 async/await:RSC 可以是异步函数,允许在组件的渲染逻辑中直接使用 async/await 进行数据查询,代码更简洁直观。  
  • 减少客户端-服务器瀑布流:传统应用中常见的一种性能瓶颈是“客户端-服务器瀑布流”:父组件需要先获取数据,渲染后,子组件才能开始获取自己的数据,导致多次低效的客户端到服务器的网络往返。RSC 允许将这种数据依赖关系移到服务器内部处理(服务器到数据库/服务的调用通常快得多),从而显著减少网络延迟,提升整体性能。服务器离数据源更近是天然的优势。  
  • 服务端瀑布流的可能性:需要注意的是,虽然客户端到服务器的瀑布流被消除了,但在服务器内部,如果 RSC 之间存在数据依赖,仍然可能产生“服务端瀑布流”。不过,框架层面通常会提供或计划提供预加载 (preloading) 等机制来优化这种情况。  

RSC 带来的数据获取方式的变革,可能会简化应用的数据层。对于页面的初始加载,直接在 RSC 中获取数据,可以避免创建专门的 API 接口,也可能减少对 React QuerySWR 等客户端数据缓存库的依赖。然而,这并不意味着数据管理变得完全简单。对于数据的更新(如表单提交后的 CUD 操作)和需要实时响应用户交互的数据,仍然需要额外的机制,例如新兴的 Server Actions或传统的 API 调用。并且,对于复杂的客户端交互状态,可能仍然需要客户端状态管理或缓存库。因此,RSC 更多的是转移了数据获取的复杂性,简化了初始加载场景,但整体复杂度是否降低取决于具体的应用需求和实现方式。  

性能提升 (Performance Gains)

结合零打包体积和高效数据获取,RSC 带来了多方面的性能提升:

  • 更快的初始加载和 FCP:服务器能更快地生成初始视图(包含 RSC 渲染结果和客户端组件的 SSR 输出),因为客户端初始需要处理的 JS 更少。  
  • 改善的 TTI:更少的 JS 下载、解析和执行时间,加上 RSC 部分无需水合,使得浏览器能够更快地达到可交互状态。  
  • SEO 友好:与 SSR 一样,服务器生成的 HTML 内容对搜索引擎爬虫友好,易于索引。  
  • 流式渲染与 Suspense 集成:RSC 与 React Suspense 完美配合,支持从服务器进行流式渲染。UI 可以被分割成块,服务器准备好一块就发送一块,客户端逐步渲染,用户可以更快看到页面内容,尤其对于数据加载较慢的部分体验提升明显。  

安全性 (Security)

  • 敏感信息隔离:API 密钥、数据库连接字符串、访问令牌以及其他敏感的业务逻辑可以安全地保留在 RSC 中,因为它们的代码永远不会发送到浏览器,从而避免了意外泄露的风险。  

缓存 (Caching)

  • 服务端结果可缓存:RSC 的渲染结果(RSC Payload 或最终生成的 HTML)可以在服务器端或 CDN 进行缓存。对于不经常变化的内容,这可以极大地提高后续请求的响应速度,并降低服务器的计算成本。  

6. 服务端与客户端组件的协作

RSC 和客户端组件 (CC) 并非孤立存在,它们可以在同一个 React 应用中组合使用,共同构建用户界面。理解它们之间的交互规则至关重要。

边界标记:"use client" 指令

  • 划分界限"use client" 指令是区分服务器和客户端组件的明确标记。它必须放置在文件的最顶部(在任何 import 语句之前)。  
  • 客户端入口:这个指令标志着从服务器环境到客户端环境的“入口点”。一旦一个文件被标记为 "use client",它以及它导入的所有其他模块(除非这些模块本身也被标记为 "use client" 或来自仅客户端的库)都会被包含在客户端 JavaScript 包中。  
  • "use server" 组件指令:再次强调,没有用于标记组件的 "use server" 指令。组件要么是默认的 RSC,要么是显式标记的 CC。("use server" 用于标记 Server Actions 函数)。  

组件组合规则

RSC 和 CC 之间的导入和渲染遵循特定的规则:

  • RSC 可以渲染 CC:服务端组件可以像渲染普通 React 组件一样,导入并渲染客户端组件。  
  • CC 不能直接导入 RSC:客户端组件不能直接 import 服务端组件。因为 RSC 的代码不存在于客户端环境中,无法被导入和执行。  
  • 通过 Props/Children 传递 RSC:为了在客户端组件内部“显示”服务端组件的内容,可以将 RSC 作为 children 或其他 prop 从父级 RSC 传递给 CC。此时,CC 接收到的不是 RSC 的代码或组件类型,而是该 RSC 在服务器上已经渲染好的结果(作为 RSC Payload 的一部分)。这种机制允许服务器和客户端组件在 UI 树中交错存在,实现灵活布局。  

这种“客户端不能导入服务端”的规则,实际上引导了一种特定的架构模式:服务端组件倾向于位于组件树的较高层级,负责数据获取、整体布局和业务逻辑编排;而客户端组件则更多地作为叶子节点,负责具体的交互逻辑和状态管理。这天然地促进了关注点分离,但也要求开发者在设计组件结构时仔细规划服务器和客户端的边界。

Prop 序列化

当 RSC 渲染 CC 并向其传递 props 时,这些 props 必须是可序列化 (Serializable) 的,就像通过网络发送数据一样。  

  • 允许的类型:基本数据类型(字符串、数字、布尔值、null、undefined)、只包含可序列化值的普通对象和数组、Promise(可以通过 Suspense 和 use Hook 处理)。  
  • 不允许的类型:函数、事件处理器、类实例、带有循环引用的复杂对象、Symbol、Map、Set、Date 对象(通常需要先转换为字符串或时间戳)等。  

Props 序列化的要求,构成了服务器环境和客户端环境之间一道清晰的“API 边界”。开发者必须像设计传统 API 的数据载荷一样,仔细考虑哪些数据可以通过这个边界从服务器传递到客户端。这不仅仅是一个技术限制,更是一个重要的架构设计约束。

状态保持 (State Preservation)

一个重要的特性是,当包含客户端组件的服务端组件树在服务器上重新渲染或重新获取数据时(例如,通过路由导航或 Server Actions 触发刷新),嵌套在其中的客户端组件在浏览器中的状态(如 useState 的值、输入框的焦点、甚至是进行中的动画)会被保留下来,不会丢失。这确保了在更新部分服务端渲染内容时,用户交互状态的连续性。  


7. 框架的角色:以 Next.js 为例

虽然 RSC 是 React 核心团队提出的概念和能力,但在实践中,要有效地使用 RSC,通常离不开元框架 (Meta-frameworks) 的支持,其中 Next.js 是目前最主要的推动者和实现者。

为什么需要框架?

从零开始实现 RSC 的支持是一项非常复杂的工程,涉及到:

  • 构建工具集成:需要与打包工具(如 Webpack, Turbopack)深度集成,能够识别 "use client" 指令,并据此进行代码分割,生成客户端和服务端不同的 bundle。  
  • 服务器运行时:需要一个服务器环境来执行 RSC 的渲染逻辑。  
  • 路由集成:需要将 RSC 的渲染与路由系统结合起来,根据路由动态渲染相应的 RSC 树。  
  • RSC Payload 处理与流式传输:需要实现 RSC 渲染结果(Payload)的生成、序列化、流式传输到客户端,以及客户端的解析和 React 树的协调更新机制。  

框架(如 Next.js, Remix 等)将这些底层复杂性进行了封装和抽象,提供了开箱即用的 RSC 开发体验。  


Next.js App Router 的实现

Next.js 的 App Router 是围绕 RSC 构建的,其实现方式体现了 RSC 的核心思想:

  • 默认 RSC:在 app/ 目录下创建的组件,默认就是服务端组件。只有添加了 "use client" 指令的组件才是客户端组件。  
  • RSC Payload:Next.js 在服务器端使用 React 的 API 将 RSC 渲染成其特有的二进制中间格式——RSC Payload。这个 Payload 包含了渲染好的 RSC 树结构、客户端组件的占位符及其对应的 JavaScript 文件引用、以及从 RSC 传递给 CC 的序列化 props。  
  • 内置流式传输:App Router 默认启用流式渲染。结合 React Suspense(通过 loading.js 文件定义路由级别的 fallback,或在组件中使用 <Suspense>),Next.js 可以将 RSC Payload 和初始 HTML 分块流式传输到客户端。  
  • Server Actions:Next.js 还引入了 Server Actions 作为 RSC 的补充,提供了一种在服务器端安全执行函数(通常用于处理表单提交或数据变更)的机制,这些函数可以从客户端组件或 RSC 中调用,进一步模糊了前后端的界限。  
  • 渲染流程:页面初始加载时,大致流程如下:服务器根据路由渲染对应的 RSC 树到 RSC Payload -> Next.js 利用 Payload 和客户端组件的构建指令,在服务器端渲染出初始 HTML -> HTML 被流式发送到浏览器,用户快速看到非交互式预览 -> RSC Payload 也被流式发送 -> 客户端 React 接收 Payload,与服务端渲染的 HTML 进行协调,更新 DOM -> 客户端组件的 JavaScript 被下载并执行水合,页面变得完全可交互。  

框架依赖性与生态考量

目前来看,想要在实际项目中应用 RSC,很大程度上需要依赖像 Next.js 这样的框架。虽然 RSC 本身是 React 的一部分,但脱离框架使用需要开发者自行处理大量复杂的底层集成工作,门槛非常高。这意味着选择采用 RSC,往往也意味着需要接受特定框架的技术栈和约束。  

同时,RSC 的强力推广主要由 Next.js (Vercel) 引领,这也引发了一些关于生态系统碎片化的担忧。如果其他框架、路由库或构建工具在对 RSC 的支持上出现滞后,或者不同实现之间存在兼容性问题,可能会导致开发者在技术选型上面临困境,第三方库的适配也可能需要针对不同框架进行。此外,迁移现有的大型 React 应用到基于 RSC 的新架构(如 Next.js App Router)本身也是一项充满挑战的任务。  


8. 挑战与考量

尽管 RSC 带来了诸多诱人的优势,但在采用它时,开发者也需要认识到其伴随的挑战和需要权衡的因素:

  • 学习曲线与心智模型转变 (Learning Curve & Mental Model Shift):RSC 引入了一系列新概念,如服务器/客户端组件的分野、边界 ("use client")、Props 序列化规则、异步组件 (async/await)、RSC Payload 等。开发者需要跳出传统纯客户端 React 的思维定式,理解这种跨服务器和客户端的混合渲染模式,这需要一定的学习成本。它在某种程度上融合了前端和后端的关注点,要求开发者具备更全面的知识。  
  • 增加的复杂性 (Complexity):虽然 RSC 旨在简化某些方面(如初始数据获取),但管理服务器/客户端边界、确保 Props 正确序列化、理解跨环境的渲染生命周期、以及与框架(如 Next.js App Router 的特定约定)的紧密耦合,都可能给大型应用的开发和维护带来新的复杂性。这种复杂性与抽象能力的权衡是开发者需要面对的。RSC 并非银弹,它在解决旧问题的同时,也引入了新的系统层面的思考维度。  
  • 调试与工具链 (Debugging & Tooling):调试跨越服务器和客户端执行的代码比纯客户端调试更具挑战性。传统的浏览器开发者工具可能不足以完全覆盖问题排查,需要结合服务器日志、框架提供的特定工具以及更完善的可观测性技术。相关的调试工具和最佳实践仍在发展和完善中。  
  • 生态系统兼容性 (Ecosystem Compatibility):许多现有的 React 库,特别是那些深度依赖 useEffectuseState 或直接操作 DOM、依赖 window 等浏览器 API 的库,可能无法直接在 RSC 中使用,或者需要库作者进行适配,提供兼容 RSC 的导出或用法。对于状态管理库、UI 组件库等,开发者需要仔细考虑它们与服务器/客户端边界的交互方式,确保状态传递和渲染的正确性。  
  • 迁移成本 (Migration Cost):将一个已有的、基于客户端渲染(如 Create React App)或传统 SSR(如 Next.js Pages Router)的大型 React 应用迁移到基于 RSC 的架构(如 Next.js App Router),往往是一项复杂且耗时的工作,可能涉及大量代码重构、路由调整、数据获取逻辑的修改,并可能遇到各种预期之外的问题。  

技术成熟度曲线

RSC 作为一个相对较新的技术(RFC 提出于 2020 年底,Next.js App Router 在 2022-2023 年间趋于稳定),其相关的生态系统,包括第三方库的支持、最佳实践、开发工具链以及社区的普遍理解程度,都还处于不断发展和成熟的过程中。早期采用者需要有准备去探索和适应这些仍在演进中的方面。  


9. 结语

React 服务端组件 (RSC) 的出现,标志着 React 应用架构发展的一个重要里程碑。它并非简单地回归服务器渲染,而是对服务器和客户端能力的一次重新审视与智能划分。

RSC 的核心价值在于,它直面了现代 Web 应用(无论是纯客户端渲染还是传统服务端渲染)普遍存在的痛点——向客户端传输过多的 JavaScript 代码。通过让一部分组件完全在服务器端运行,并且其代码和依赖不进入客户端包,RSC 从根本上解决了这个问题,带来了显著的性能优势:更小的包体积、更快的初始加载、更短的可交互时间,以及更低的客户端计算负担。

同时,RSC 提供的服务端直接数据获取能力、与 Suspense 的无缝集成带来的流式渲染体验、以及增强的安全性,共同构成了其强大的吸引力。它使得开发者能够构建出既具备 SPA 丰富交互性,又拥有接近传统 MPA 性能优势的应用。  

当然,RSC 也并非没有代价。它引入了新的概念和规则,带来了学习曲线和额外的复杂性,生态系统的成熟度和兼容性仍在发展中,且目前很大程度上依赖于 Next.js 等元框架的支持。

总而言之,React 服务端组件代表了 React 生态系统在追求极致性能和更优开发体验道路上的重要一步。虽然存在挑战,但它所展现的潜力——构建更快、更轻、更强大的 Web 应用——使其成为未来 React 开发的关键方向之一。对于 React 开发者而言,理解 RSC 的核心思想、优势与局限,并关注其生态发展,将是在未来构建高性能 Web 应用的必备技能。