Gatsby 部分水合(Partial Hydration)完全指南:基于 React Server Components 的选择性交互架构
2026/9/19 14:57:23 网站建设 项目流程

Gatsby 部分水合(Partial Hydration)完全指南:基于 React Server Components 的选择性交互架构

【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby

本指南围绕 Gatsby 5 引入的 Partial Hydration(部分水合)功能展开,系统讲解它如何借助 React Server Components,让原本全站静态的页面只对真正需要交互的组件下发并执行 JavaScript,从而显著改善 Time to Interactive(可交互时间)等前端性能指标。读完本文,你将掌握 Partial Hydration 的概念模型、Gatsby 内部的 RSC 构建原理,以及如何在真实项目中声明客户端组件、划定水合边界并规避已知限制。

从全量水合到部分水合

什么是水合(Hydration)

Gatsby 的核心设计之一是:在构建期使用 React 的ReactDOMServer将页面渲染成静态 HTML,发布到/public目录;浏览器拿到这份 HTML 后,再由客户端 JavaScript 为服务器渲染好的标记"接续"状态与交互能力——这个过程就是水合(Hydration,有时也写作 re-hydration,二者等价)。水合的本质是"用客户端 JavaScript 为服务端渲染的 HTML 添加应用状态与交互性"。

传统 SSR/静态站点存在一个天然矛盾:gatsby build产出的 HTML 立即可见(First Contentful Paint 很快),但事件监听器要等 JavaScript 下载、解析、执行完毕后才挂载。用户看到"貌似可点击"的按钮却无法真正点击,这种"UI 已渲染但尚未可交互"的窗口期在业内被称为 uncanny valley(恐怖谷效应),直接拖累 Time to Interactive 指标。Gatsby 也据此在自己的水合概念指南中完整阐述了这一过程:每次首访,响应返回静态 HTML 与关联的 JS/CSS/图片,随后 React 通过hydrateRoot()接管 DOM、挂载事件监听,把站点变成完整的 React 应用;此后的页面跳转则完全是 React 管理的 DOM 更新。

全量水合的问题

从 Gatsby 诞生到 4.x,凡是 Gatsby 构建的站点,在客户端都会做全量水合:无论页面里有多少静态内容(页头、页脚、纯文本段落),浏览器都必须下载整套页面 JavaScript、由 React 为整棵组件树创建 VDOM 节点并逐一求值。结果是——为那些根本不需要交互的部分也白白传输并执行了 JavaScript。

部分水合的思路

Partial Hydration 的理念恰恰相反:只对页面中"孤岛"式的交互区域下发并水合 JavaScript,其余部分保持纯静态 HTML。下图直观对比了两种模式——左侧是全量水合,整个浏览器窗口(含静态内容)都被标记为蓝色,表示整页参与水合;右侧是部分水合,只有交互式画廊区域被标记。

你可以把 Partial Hydration 理解为一种"超级代码分割":如果某个重型库只在服务端渲染阶段被用到,就完全不必打包发给客户端。客户端 JavaScript 越少,最直接的收益是 TTI(可交互时间)缩短;同时也能缓解"恐怖谷效应"——让用户看到的 UI 与真正可交互的 UI 尽快一致。下图展示了水合在页面渲染时间线中的位置:服务端返回 HTML 并完成绘制后,JS 仍需"到达 → 处理 → 水合"才能让 UI 可交互,这一整段等待正是部分水合要压缩或消除的部分。

值得澄清的是:部分水合与近年流行的 "island architecture"(孤岛架构,如 Astro 所推行)最终效果相似——页面上出现一个个可独立水合的交互孤岛,但实现路径截然不同。Gatsby 之所以选择 React Server Components 而非孤岛架构,核心原因是让开发者基本延续原有的书写习惯(详见下文"为什么是 React Server Components")。

Gatsby 中部分水合的工作原理

Gatsby 的 Partial Hydration 建立在 React Server Components(RSC)之上,详细设计可参考 React 官方的 Server Components RFC(外部资料,仅作背景参考)。

默认全部是服务端组件

开启功能后,Gatsby 从顶层页面(如src/pages下的页面,或通过createPageAPI 创建的页面)开始,默认把所有组件标记为服务端组件。除非用"use client"指令显式声明,否则组件不会向客户端下发任何 JavaScript——这是"开箱即用的性能提升"的来源。

页面请求从 JS 变为 RSC 描述文件

在部分水合模式下,浏览器不再像传统模式那样请求页面组件的 JavaScript 文件,而是请求page-data-rsc.json。这个 JSON 文件是 UI 的描述:其中客户端组件以 bundle 引用(reference)的形式出现,浏览器再依据引用去加载组件真正的代码。这也解释了为什么 server 组件向 client 组件传递的 props 必须可序列化——它们会被写入 JSON 文件;函数、回调等无法序列化的值自然无法传递。

"use client" 指令与组件边界

"use client"是 React 的服务端模块约定中的关键指令,作为文件的第一行代码出现。它声明了服务端与客户端组件之间的边界:一个模块只要标记了"use client",它(及其依赖的模块)就整体进入客户端包,其渲染出的 HTML 会在客户端被水合。服务端组件与客户端组件可以在应用中混用,React 会在幕后把它们合并到同一棵组件树里,如下图的组件树所示:服务端组件可以包含服务端与客户端组件,客户端组件也可以嵌套服务端与客户端组件。

构建期的清单(manifest)生成

在构建过程中,Gatsby 会扫描所有组件,生成一张客户端组件清单,记录每个客户端组件及其对应的 chunk;再依据该清单产出 RSC 输出。这一点可以直接在仓库源码中得到印证:PartialHydrationPlugin 通过 webpack 的 parser hook 检查 AST 中是否存在use client指令(statement.directive === "use client"),命中后把module.buildInfo.rsc置为true以标记客户端模块;随后在processAssets阶段调用_generateManifest,从compilation.moduleGraphchunkGraph中收集各客户端模块的导出与 chunk 编号,生成映射所有客户端组件及其独立 chunk 的清单文件,并在增量构建时合并上一次的清单以保留缓存(this._previousManifest)。

与清单配合的还有 partial-hydration-reference-loader:它对包含"use client"的模块进行解析,把每个具名导出与默认导出改写为$$typeof: Symbol.for('react.module.reference')形式的模块引用对象,并附带filepath与导出名——这正是 RSC 输出中"客户端组件以 bundle 引用形式出现"的底层实现,也是page-data-rsc.json能够以最小体积描述 UI 的原因。

为什么是 React Server Components,而非孤岛架构

如果没接触过 RSC,建议先观看 React 官方介绍 Server Components 的演讲或阅读 RFC(外部资料)。Gatsby 选择 RSC 实现部分水合,而没有倒向孤岛架构,理由可以概括为:让你(大体上)继续用习惯的方式写应用

在孤岛架构的世界里,开发者要分别创作一个个"孤岛层",每个孤岛拥有独立的上下文(context)与 React 树。这意味着:

  • 无法简单地在孤岛间共享 context,或让 React 事件在孤岛间向上/向下冒泡,必须自行编写连接逻辑;
  • 思维方式需要转变——创建应用时要把"静态壳"与"交互孤岛"当作不同的实体来组织;
  • 孤岛无法渲染整页,因而不适用于 SPA,会导致页面间导航更迟缓。

而 React Server Components 让应用的大部分仍保持原来的写法,只需遵循若干约束,而不是一场彻底的范式转换。其附带收益包括:

  • 对 bundle 体积零影响:服务端组件代码根本不会进入客户端 bundle;
  • 与客户端组件无缝集成:二者在同一组件树中共存、由 React 统一协调;
  • 子树/组件级更新并保留客户端状态:服务端返回的 RSC 流可以只更新局部子树,同时保持客户端组件的既有状态。

实战:在 Gatsby 5 中启用部分水合

概念之外,Gatsby 还提供了对应的操作指南(以下步骤、代码与该指南保持一致)。

前置条件

  1. 一个基于gatsby@5.0.0或更高版本的项目(可从快速开始起步);

  2. 安装react@experimentalreact-dom@experimental

    npm install --save-exact react@experimental react-dom@experimental --legacy-peer-deps
  3. gatsby-config.js中开启PARTIAL_HYDRATION标志:

    module.exports = { flags: { PARTIAL_HYDRATION: true } }

关于该标志位,源码提供了更多可验证的细节:flags.ts 中定义:

  • 标志名PARTIAL_HYDRATION,对应环境变量GATSBY_PARTIAL_HYDRATION,telemetry 标识为PartialHydration
  • command: "build"——即该功能只在构建类命令下生效;
  • experimental: true——当前处于实验阶段;
  • testFitness校验:仅当GATSBY_MAJOR === "5"且本机 React 满足>=18.0.0(或^0.0.0的 experimental 版本)时才可启用;不满足时会给出明确提示:"Partial hydration requires React 18+ to work."

服务端组件(默认行为)

启用后所有组件默认是服务端组件gatsby build产出的 HTML 在客户端不依赖任何 JavaScript,性能提升开箱即得;Gatsby 从顶层页面(src/pagescreatePage)开始生成服务端组件。只有在组件确实需要交互时,才需要显式标记为客户端组件。

客户端组件("use client")

客户端组件的 HTML 会在客户端被水合——即用客户端 JavaScript 为服务端渲染的 HTML 添加状态与交互。声明方式是在文件第一行写入"use client"指令:

"use client" import * as React from "react" const Joke = () => { const [isShown, show] = React.useReducer(() => true, false) return ( <main> <button onClick={show}>Show me a joke</button> {isShown && <p>Why couldn’t the React component understand the joke? Because it didn’t get the context.</p>} </main> ) } export default Joke

何时应使用客户端组件

官方建议:默认用服务端组件,只对确有必要的地方选择性地声明客户端组件。典型的客户端组件使用场景包括:

  • 需要交互与事件监听(onClick()onChange()等);
  • 需要状态与生命周期(useState()useEffect()等);
  • 需要访问浏览器专有 API(如读取window上的属性);
  • React 类组件。

组件边界与组件树优化的最佳实践

不必给每个交互组件都加指令

"use client"只需加在被服务端组件导入的组件上,由此在服务端与客户端之间划出边界;客户端组件内部再导入的其他客户端组件无需重复声明。例如:src/pages/index.jsx导入了<SocialMedia>,而<SocialMedia>又导入<Instagram><Twitter>,三者都用到了useEffect()——由于只有<SocialMedia>是被服务端组件导入的客户端组件,只需在<SocialMedia>上加"use client"即可。

把客户端组件推向组件树叶子

组织组件结构时,应尽可能把客户端组件放到组件树的叶子节点,以最小化下发到客户端的 JavaScript。假如共享布局组件里有一个展示最新推文的交互式页脚,不要给整个布局加"use client",而应把页脚拆成独立组件,只标记它:

"use client" import * as React from "react" const Footer = () => { React.useEffect(() => { // do fetching stuff }) return ( <footer>My Tweets</footer> ) } export default Footer
import * as React from "react" // Footer is a client component import Footer from "./footer" const Layout = ({ children }) => ( <> <main>{children}</main> <Footer /> </> ) export default Layout

服务端组件不能导入客户端组件,但可以作为 children 传入

服务端组件不能被导入客户端组件,但可以以childrenprop 的形式传入客户端组件,让 React 同时实例化二者:

"use client" import * as React from "react" export const MyClientComponent = ({ children }) => ( <div> <p>Re-Hydrated on the client</p> {children} </div> )
import * as React from "react" import { MyServerComponent } from "../components/my-server-component" import { MyClientComponent } from "../components/my-client-component" const Page = () => ( <MyClientComponent> <MyServerComponent /> </MyClientComponent> ) export default Page

props 必须可序列化

服务端组件向客户端组件传 props 的方式与往常基本一致,但必须可序列化——函数、回调等无法写入 JSON 的值不能传递:

// OK const Page = () => <ClientComponent color="rebeccapurple" /> // ⚠️ Doesn't work const Page = () => ( <ClientComponent onClick={() => console.log("Hello World")} /> )

当前限制(务必知悉)

使用 Partial Hydration 前请确认以下限制:

  • 必须使用 React 的experimental 发布版,官方不建议在生产环境使用
  • React 生态中大量包尚未适配 React Server Components(例如 CSS-in-JS 解决方案);
  • Partial Hydration 仅在gatsby buildgatsby serve下生效,不适用于gatsby develop——这与源码中标志位command: "build"的定义完全一致。

深入阅读

  • 概念篇:React 水合(Hydration)概念指南——了解全量水合与hydrateRoot()的工作原理
  • 操作篇:使用 Partial Hydration 实战指南——包含完整的启用步骤与 FAQ
  • 源码篇:标志位定义、PartialHydrationPlugin 与 partial-hydration-reference-loader——深入 RSC 清单生成与模块引用改写的实现细节

【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询