关于 progressive-hydration
面向 React SSR 开发者的渐进式水合指南,介绍如何分阶段激活页面组件并延后非关键部分,以减少初始 JavaScript 负担。
主要功能
- 01延后非关键组件水合
- 02按需加载页面组件
- 03结合代码拆分减少脚本
- 04根据可见性或触发条件水合
适用场景
- 01让 SSR 页面关键区域先变为可交互
- 02延迟视口外或少用组件的加载
- 03减少初始加载所需的 JavaScript
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill progressive-hydration安装后在 Claude Code 中输入/progressive-hydration确认已加载。
详细介绍
渐进式水合
服务端渲染的应用会使用服务端为当前导航生成 HTML。服务端完成 HTML 内容的生成后(其中还包含正确显示静态 UI 所需的 CSS 和 JSON 数据),便会将这些数据发送给客户端。由于标记是由服务端生成的,客户端可以快速解析并将其显示在屏幕上,从而实现快速的首次内容绘制!
虽然服务端渲染能更快地实现首次内容绘制,却不一定能缩短可交互时间。让网站能够响应交互所需的 JavaScript 尚未加载。按钮_看起来_可能可以交互,但实际上(暂时)还不能交互。只有在 JavaScript 包加载并处理完成后,处理程序才会附加上去。这个过程称为_水合_:React 会检查当前 DOM 节点,并使用对应的 JavaScript 对这些节点进行水合。
适用场景
- 当 SSR 应用包含不需要立即响应交互的非关键部分时,使用此方法
- 这有助于减少页面首次加载时让页面可交互所需的 JavaScript
不适用场景
- 整个页面都支持交互,且所有组件都需要立即水合时
- 管理水合边界的复杂性超过性能收益时
- 页面较小、JavaScript 总量已经很少且水合速度很快时
说明
- 将非关键组件放入带有适当回退内容的
<Suspense>边界中 - 使用
React.lazy()和代码拆分,延迟加载首屏以下或很少使用的组件 - 在 React 18+ 中,利用选择性水合自动优先处理用户交互过的区域
- 在 Next.js 中,对纯客户端组件使用
next/dynamic和{ ssr: false }
详情
用户看到屏幕上的 UI 暂时无法交互,这段时间也被称为“恐怖谷”:用户可能以为可以与网站交互,但组件尚未附加处理程序。这可能会让用户感到沮丧,因为 UI 看起来像是冻结了!
从服务端接收的 DOM 组件可能需要一段时间才能完全水合。在组件水合之前,必须先加载、处理并执行 JavaScript 文件。我们不必像之前那样一次性水合整个应用,也可以对 DOM 节点进行_渐进式水合_。渐进式水合可以随着时间推移逐个水合节点,因此只需请求必要的最少量 JavaScript。
通过渐进式水合应用,我们可以延迟水合页面中不太重要的部分。这样一来,我们就能减少让页面可交互所需请求的 JavaScript 数量,并且只在用户需要时才水合节点。渐进式水合也有助于避免最常见的 SSR 再水合问题,即服务端渲染的 DOM 树被销毁后又立即重建。
渐进式水合允许我们根据特定条件水合组件,例如组件在视口中可见时。
渐进式水合的实现
在使用 React 实现 SSR 的章节中,我们讨论了为服务端渲染的应用进行客户端水合。水合让客户端 React 能够识别服务端渲染的 ReactDOM 组件,并为这些组件附加事件。因此,当 SSR 应用在客户端可用后,水合能让它持续、无缝地像 CSR 应用一样运行。 要让页面上的所有组件都能通过水合变为可交互状态,这些组件的 React 代码就应包含在下载到客户端的代码包中。高度交互、主要由 JavaScript 控制的 SPA 需要一次性加载整个代码包。然而,页面大多静态、只有少数交互元素的网站,可能不需要所有组件一开始就处于活动状态。对于这类网站,为屏幕上的每个组件都发送一个庞大的 React 代码包会带来额外开销。
渐进式水合通过允许我们在页面加载时只水合应用的某些部分来解决这个问题。其他部分则会根据需要逐步水合。
整个应用不会一次性初始化,而是从 DOM 树的根节点开始水合,但服务器渲染应用中的各个部分会在一段时间内逐步激活。水合过程可以在不同分支处暂停,并在这些分支进入视口或满足其他触发条件时恢复。请注意,执行每次水合所需资源的加载也会通过代码拆分技术延迟,从而减少让页面变为可交互状态所需的 JavaScript 代码量。
渐进式水合背后的理念是分块激活应用,从而提供出色的性能。任何渐进式水合解决方案都应考虑其对整体用户体验的影响。全面实现渐进式水合需要满足以下要求:
- 允许对所有组件使用 SSR。
- 支持将代码拆分为单个组件或代码块。
- 支持按照开发者定义的顺序在客户端水合这些代码块。
- 不阻止用户与已完成水合的代码块进行交互。
- 允许为延迟水合的代码块使用某种加载指示器。
当 React concurrent mode 向所有人开放后,它将满足以上所有要求。它允许 React 同时处理不同任务,并根据给定的优先级在任务之间切换。切换时,无需提交部分渲染的树,因此 React 切回同一任务后,渲染任务可以继续进行。
可以使用 React concurrent mode 实现渐进式水合。在这种情况下,页面上每个代码块的水合都会成为 React concurrent mode 的一项任务。如果需要执行用户输入等优先级更高的任务,React 就会暂停水合任务,转而处理用户输入。lazy()、Suspense() 等功能允许你声明式地设置加载状态。可以用它们在代码块延迟加载时显示加载指示器。SuspenseList() 可用于定义组件延迟加载的优先级。
React concurrent mode 还可以与另一项 React 功能结合使用:
- 服务器组件。这允许你从服务器重新获取组件,并在组件流式传入时于客户端渲染,而不必等到整个请求完成。这样一来,即使我们在等待网络请求完成,客户端的 CPU 也能继续工作。
目前已有多种渐进式水合的实现方案。一个使用 Preact 和 Next.js 实现部分水合的 POC 使用了:
pool-attendant-preact:一个使用 Preact x 实现部分水合的库。next-super-performance:一个使用该库来提升客户端性能的 Next.js 插件。
pool-attendant-preact 库包含一个名为 withHydration 的 API,可让你标记需要水合、交互性更强的组件。这些组件会优先水合:
import Teaser from "./teaser";
import { withHydration } from "next-super-performance";
const HydratedTeaser = withHydration(Teaser);
export default function Body() {
return (
<main>
<Teaser column={1} />
<HydratedTeaser column={2} />
<HydratedTeaser column={3} />
<Teaser column={1} />
<Teaser column={2} />
<Teaser column={3} />
<Teaser column={1} />
<Teaser column={2} />
<Teaser column={3} />
</main>
);
}
第 2 列和第 3 列中的组件 HydratedTeaser 会优先水合。现在,你可以使用 hydrate() API 在客户端水合其余组件:
import { hydrate } from "next-super-performance";
import Teaser from "./components/teaser";
hydrate([Teaser]);
优缺点
渐进式水合在提供服务端渲染和客户端水合的同时,也能降低水合成本:
-
促进代码拆分:代码拆分是渐进式水合不可或缺的一部分,因为需要为各个延迟加载的组件创建代码块。
-
允许按需加载不常用的页面部分:页面中可能有一些组件大多是静态的、位于视口之外,和/或不常用。这类组件非常适合延迟加载。
-
**减小包体积:**代码拆分会自动减小包体积。加载时需要执行的代码更少,有助于缩短 FCP 与 TTI 之间的时间。
另一方面,渐进式水合可能不适用于动态应用,因为这类应用中的每个屏幕元素都对用户可用,并且需要在加载时变为可交互。这是因为,如果开发者不知道用户可能首先点击哪里,就可能无法确定应优先对哪些组件进行水合。