我不知道的 React:服务器组件(RSC) vs SSR 与水合详解

739 字 18 min read
React RSC SSR Server Components Hydration 性能优化 前端开发

React 的生态系统持续演进,服务器组件 (React Server Components, RSC) 的引入无疑是近年来最引人注目的变化之一。然而,RSC、服务器端渲染 (Server-Side Rendering, SSR) 以及水合 (Hydration) 这几个概念常常被混淆。它们都与服务器端有关,但解决的问题和工作方式却大相径庭。让我们来彻底厘清它们。

React 服务器组件 (RSC) 解密

什么是 RSC?

React Server Components (RSC) 是一种只能在服务器上运行和渲染的 React 组件类型。

核心设计目标: 💡

  1. 零客户端包体积 (Zero Bundle Size): RSC 及其依赖的库完全不会被打包进客户端的 JavaScript 文件中。这意味着即使 RSC 内部使用了大型库(如日期处理、Markdown 解析库),这些库的代码也不会发送到浏览器,从而显著减小客户端需要下载和解析的 JS 体积,加快应用加载和交互速度。🚀
  2. 直接后端访问 (Direct Backend Access): 由于在服务器环境运行,RSC 可以像写后端代码一样,直接、安全地访问数据库、文件系统、内部微服务或调用敏感 API,无需像客户端组件那样需要额外的 API 接口和请求处理。
  3. 自动代码分割 (Automatic Code Splitting): RSC 及其依赖自然地按需加载。只有当导航到需要某个 RSC 的页面或部分时,对应的服务器端代码才会被执行和渲染。
  4. 流式渲染与 Suspense 集成 (Streaming & Suspense Integration): RSC 可以利用 async/await 直接在组件级别进行数据获取。结合 React.Suspense,当 RSC 在服务器端等待数据时,React 可以先将 Suspensefallback UI(如加载指示器)流式传输到客户端显示,待数据准备好后,再将 RSC 的实际渲染结果作为后续数据块发送。这避免了瀑布流请求,并能在数据加载期间提供即时反馈,改善用户感知的性能。

RSC 的产物: RSC 在服务器上执行后,渲染的不是 HTML,而是一种特殊的中间格式 (RSC Payload)。这种 Payload 本质上是一个序列化的 React 元素树,描述了 UI 结构、props,以及对需要渲染的客户端组件(Client Components)的引用及其所需 props。这个 Payload 需要客户端的 React runtime 来解析并渲染到 DOM(或与 SSR 生成的 HTML 结合)。

RSC vs. 客户端组件 (Client Components)

理解 RSC 最好的方式是与我们熟悉的、在浏览器中运行的组件(现在官方称为客户端组件, Client Components)进行对比:

特性 服务器组件 (RSC) 客户端组件 (Client Component)
运行环境 服务器 (构建时或请求时) 浏览器 (客户端)
状态 (State) 不能 使用 useState, useReducer ✅ 可以使用
生命周期/Effects 不能 使用 useEffect, useLayoutEffect ✅ 可以使用
浏览器 API 不能 访问 window, document ✅ 可以访问
交互性 (事件) 不能 添加事件监听器 (onClick 等) ✅ 可以添加事件监听器,处理用户交互
自定义 Hook 限制 不能 使用依赖状态或 Effect 的 Hook ✅ 可以使用所有类型的 Hook
后端数据访问 ✅ 可以直接访问数据库、文件系统等 ❌ 需要通过 API 请求
与 Suspense 服务端数据获取天然配合 Suspense 客户端异步操作可配合 Suspense
发送到客户端 渲染结果 (RSC Payload) JavaScript 代码 (组件及其依赖)

核心取舍: RSC 通过放弃交互性和客户端状态管理能力,换取了零客户端包体积、直接后端访问和与 Suspense 结合的优势。客户端组件则承担所有用户交互和浏览器环境下的动态任务。一个典型的现代 React 应用会是两者的组合。

RSC vs. SSR 辨析:目标与产物的差异

这是最关键的区分点。虽然都涉及服务器,但 RSC 和 SSR 是为了解决不同问题而设计的两种技术:

  1. 核心目标不同: 🎯

    • SSR 主要解决: 首屏加载性能 (FCP - First Contentful Paint)SEO。通过在服务器直接生成 HTML,浏览器可以更快地显示内容,搜索引擎也能抓取到页面信息。
    • RSC 主要解决: 客户端 JavaScript 负担过重(减少 Bundle Size)、数据获取瀑布流(通过服务端获取和 Suspense 改善加载体验)、以及简化后端数据访问
  2. 产物不同: 📦

    • SSR 的产物: HTML 字符串。这是浏览器可以直接解析和渲染的标准格式。
    • RSC 的产物: RSC Payload (特殊的序列化 VDOM 指令流)。这不是 HTML,需要客户端 React 来解析和应用。
  3. 对组件类型的限制: ⚠️

    • SSR: 可以渲染包含客户端逻辑(如 useState, useEffect)的组件,但这些 Hook 在服务器端仅用于初始状态渲染,其交互逻辑和副作用不会在服务器执行。
    • RSC: 严格区分服务器组件和客户端组件。服务器组件绝对不能包含任何客户端逻辑 (如 useState, useEffect 或依赖它们的自定义 Hook)。

联系与结合: RSC 和 SSR 不是互斥的,它们可以而且经常一起使用(例如在 Next.js App Router 中)。在这种模式下:

  • 服务器首先通过 SSR 生成页面的初始 HTML 骨架,这有助于快速 FCP 和 SEO。
  • 对于页面中标记为 RSC 的部分,服务器执行它们并生成 RSC Payload,这个 Payload 会嵌入到初始 HTML 响应中或通过流式传输发送。
  • 对于标记为客户端组件的部分,SSR 会渲染其初始 HTML,但其对应的 JS 代码会被打包发送到客户端。
  • 客户端 React 接收到 HTML 和 RSC Payload 后,会进行"水合"和渲染,最终构建出完整的、可交互的页面。

简单总结: SSR 是将整个 React 应用(或页面)在服务器"拍照"生成 HTML;RSC 是一种让特定组件在服务器执行并将其结果(非 HTML)发送给客户端的策略,旨在优化 JS 负载和数据流。两者结合能发挥各自优势。

"水合"(Hydration) 过程详解

谈论 SSR,就必然会涉及到"水合"。那么,水合具体是什么?需要强调的是,传统意义上的水合主要针对 SSR 生成的静态 HTML。

什么是水合? 💧

水合 (Hydration) 是指客户端的 React "激活"由服务器端渲染 (SSR) 生成的静态 HTML 页面的过程,主要是为了附加事件监听器并使页面变得可交互。

水合流程: 🚶‍♂️

  1. 服务器渲染 (SSR): 服务器执行 React 代码,生成页面的完整 HTML 结构,并将其发送给浏览器。
  2. 浏览器解析与显示: 浏览器接收到 HTML,快速解析并渲染出静态页面内容。用户此时能看到界面 (FCP 快),但页面上的按钮、输入框等还不能响应用户操作。
  3. 加载客户端 JavaScript: 浏览器开始下载页面所需的 JavaScript 包,其中包含 React 库本身以及你的客户端组件代码。
  4. 水合阶段 (React 接管):
    • 客户端 JavaScript 加载并执行完毕后,React 开始工作。
    • React 会在内存中根据客户端组件代码重新构建出页面的虚拟 DOM (VDOM) 树
    • 然后,React 会遍历这个 VDOM 树,并将其与服务器生成的现有 HTML DOM 结构进行逐一比对
    • 在比对过程中,React 主要做两件事:
      • 验证 DOM 结构一致性: 确保客户端渲染结果与服务端输出的 HTML 结构匹配。如果不匹配(通常是代码问题或数据不一致),React 会发出警告,并可能尝试修复差异,但这通常效率不高且可能导致非预期结果。
      • 附加事件监听器: 将 JavaScript 代码中定义的事件处理器(如 onClick)附加到对应的真实 DOM 节点上。
    • 水合完成后,页面就从静态 HTML 变成了完全可交互的 React 应用。

水合的性能代价: 💰

水合过程并非"免费午餐",它有显著的性能成本:

  • JS 下载与执行: 需要下载、解析和执行客户端 JavaScript 代码。
  • VDOM 重建: React 需要在客户端重新执行组件逻辑来构建 VDOM 树。
  • DOM 比对: 遍历 VDOM 和真实 DOM 并进行比对是一个 CPU 密集型操作。
  • 阻塞主线程: 上述过程,尤其是 VDOM 重建和比对,可能会长时间占用浏览器主线程,导致页面虽然看起来已经加载完成 (FCP),但实际上无法响应用户输入 (TTI 延迟),造成不好的用户体验。

RSC 与水合的关系: 🤝

  • 纯 RSC 部分: 由 RSC 渲染的内容输出的是 RSC Payload,客户端 React 直接解析这个 Payload 来更新 DOM,不需要经历传统水合中"客户端重建 VDOM 并与 HTML 比对"的昂贵步骤。这部分内容的"激活"成本很低。
  • 客户端组件部分 (即使在 RSC+SSR 模式下): 对于那些标记为客户端组件 ('use client') 的部分,即使它们最初由 SSR 渲染成了 HTML,它们仍然需要通过水合来附加事件监听器和激活交互逻辑。SSR 只是提供了初始的静态 HTML,交互性还得靠客户端 JS 和水合来赋予。
  • 整体效果: 在 RSC + SSR 结合的模式下,RSC 的使用有助于减少需要水合的组件数量和复杂性,因为大量的静态内容可以由 RSC 处理,从而降低了整体的水合开销。

水合优化探索: 💡 由于水合的成本,社区和 React 团队一直在探索优化方案:

  • 选择性水合 (Selective Hydration): React 18 引入的特性,允许在 Suspense 边界内,优先水合用户正在交互的部分,而不是一次性水合整个页面。
  • 部分水合 (Partial Hydration): 框架级优化(如 Astro Islands),只对页面中明确需要交互的"岛屿"(组件)进行水合,其余部分保持纯静态 HTML。
  • 可恢复性 (Resumability): 更进一步的理念(如 Qwik 框架),目标是完全消除水合步骤。它通过在服务器端序列化所有必要的状态和事件监听信息,使得客户端 JavaScript 可以直接"恢复"执行,而无需重新构建 VDOM 或重新附加事件。

场景选择:何时使用?

技术 主要优势 主要劣势 核心适用场景
SSR 快速 FCP, 良好 SEO 需水合才能交互, 服务端负载增加 内容型网站 (博客, 新闻), 电商, 对首屏和 SEO 要求高的应用
RSC 零客户端 JS 体积, 直接后端访问, 简化数据流 无交互性, 不能用客户端 Hooks, 需要框架支持 展示型组件, 需访问后端资源的组件, 优化大型应用 JS 负载
CSR (Client-Side Rendering) 开发简单, 服务端负载低 FCP 慢, SEO 挑战大, JS 包体积大 管理后台, 强交互应用 (对 SEO 和首屏要求不高)
RSC + SSR 结合 SSR 和 RSC 的优点: 快速 FCP, SEO 友好, 减少 JS 负担, 优化数据流 架构相对复杂, 需要现代框架支持 (如 Next.js) 大部分现代高性能 Web 应用, 复杂项目

选择策略总结: 对于需要良好 SEO 和快速首屏的应用,SSR 是基础。在此基础上,通过引入 RSC 将非交互、重数据访问的组件移到服务器执行,可以显著优化客户端性能。现代框架(如 Next.js App Router)默认推荐 RSC + SSR 的混合模式,让开发者能根据组件特性选择最合适的渲染方式,是构建高性能、体验良好、可维护性强的复杂 React 应用的有力武器。

理解 RSC、SSR 和水合的原理、区别与联系,是掌握现代 React 开发范式的关键。✨


核心问题回顾

  1. RSC 和 SSR 最本质的区别是什么?它们各自的产物是什么?
    • 答案: 本质区别在于目标和产物。SSR 目标是优化 FCP 和 SEO,产物是 HTML 字符串。RSC 目标是减少客户端 JS 负担、优化数据流和简化后端访问,产物是特殊的序列化 VDOM 指令流 (RSC Payload),而非 HTML。
  2. 传统意义上的"水合"主要发生在什么场景下?RSC 自身需要水合吗?
    • 答案: 传统水合主要发生在 SSR 场景下,用于激活服务器渲染的静态 HTML。RSC 本身输出 Payload,由客户端 React 解析更新 DOM,不经历传统水合的比对过程。但在 RSC+SSR 结合时,由 SSR 渲染的包含客户端组件的 HTML 部分仍需水合
  3. 水合过程可能带来哪些性能瓶颈?提及至少一种优化水合的技术方向。
    • 答案: 瓶颈在于客户端需要下载执行 JS、重建 VDOM、比对 DOM 并附加事件,这可能阻塞主线程,延迟 TTI。优化方向包括 React 18 的选择性水合 (Selective Hydration)、框架级的部分水合 (Partial Hydration) 或更前沿的可恢复性 (Resumability)