- Published on
React渲染模式 —— 渐进式水合
- Authors
- Name
- Deng Hua
文章翻译至: Progressive Hydration
目录
Progressive Hydration
服务器渲染的应用程序使用服务器来生成所需的 HTML。服务器完成生成 HTML 内容(其中还包含显示静态 UI 所需的 CSS 和 JSON 数据)后,会将数据发送到客户端。
由于服务器为我们生成了标记,因此客户端可以快速解析它并将其显示在屏幕上,从而产生快速的首次内容绘制(FCP)。
尽管服务器渲染提供了更快的首次内容绘制,但它并不总是能提供更快的页面交互。因为尚未加载与网站交互所需的 JavaScript。按钮可能看起来是交互式的,但实际上点击后并无反应。仅当 JavaScript 包加载并处理后,处理程序才会被附加。
这个过程称为Hydration(水合):React 检查当前的 DOM 节点,并使用相应的 JavaScript 水合节点。
用户在屏幕上看到非交互式用户界面的时间也被称为"uncanny-valley"现象:尽管用户可能认为他们可以与网站进行交互,但组件尚未附加处理程序。用户可能会感到奇怪,因为界面看起来好像被冻结了!
从服务器接收到的 DOM 组件可能需要一段时间才能完全水合。在组件被水合之前,需要加载、处理和执行 JavaScript 文件。我们不像之前那样一次性对整个应用程序进行水合,而是可以逐步对 DOM 节点进行水合。渐进水合使得随着时间的推移单独水合节点成为可能,这使得仅请求最少的必须JavaScript成为可能。
通过逐步水合应用程序,我们可以延迟页面不太重要部分的水合。通过这种方式,我们可以减少为了使页面具有交互性而必须请求的 JavaScript 数量,并且仅在用户需要时才对节点进行水合。
渐进式水合还有助于避免最常见的 SSR ReHydration 陷阱,即服务器渲染的 DOM 树被破坏然后立即重建。
渐进水合允许我们仅根据特定条件对组件进行水合,例如当组件在视口中可见时。在下面的示例中,我们有一个用户列表,一旦该列表位于视口中,该列表就会逐渐被水合。当组件水合时,会出现紫色闪光!
虽然发生得很快,但您可以看到初始 UI 与水合状态下的 UI 相同。
由于最初的 HTML 包含相同的信息和样式,因此我们可以无缝地使组件交互,而无需任何华而不实或跳跃的 UI。渐进式水合作用可以有条件地使某些组件具有交互性,而应用程序的用户可能完全不会注意到这一点。
实现Progressive Hydration
在使用 React 实现 SSR 的部分中,我们讨论了在服务器上渲染的应用程序的客户端水合作用。 Hydration 允许客户端 React 识别在服务器上渲染的 ReactDOM 组件并将事件附加到这些组件。因此,它为 SSR 应用程序引入了连续性和无缝性,一旦在客户端上可用,就可以像 CSR 应用程序一样运行。
为了使页面上的所有组件通过水合变得可交互,这些组件的 React 代码应包含在下载到客户端的包中。主要由 JavaScript 控制的高度交互的 SPA 将立即需要整个捆绑包。然而,大多数静态网站在屏幕上有一些交互元素,可能不需要所有组件立即处于活动状态。对于此类网站,为屏幕上的每个组件发送巨大的 React 包会成为一种开销。
渐进式水合通过允许我们在页面加载时仅对应用程序的某些部分进行水合来解决这个问题。其他部分根据需要逐渐水合。
水合步骤不是立即初始化整个应用程序,而是从 DOM 树的根部开始,但服务器渲染应用程序的各个部分会在一段时间内激活。各个分支的水合过程可能会被阻止,并在它们进入视口或基于某些其他触发器时恢复。请注意,执行每次水合作用所需的资源加载也会使用代码分割技术来推迟,从而减少使页面具有交互性所需的 JavaScript 数量。
渐进式水合背后的想法是通过分块激活您的应用程序来提供出色的性能。任何渐进式补水解决方案还应考虑它将如何影响整体用户体验。您不能让屏幕块一个接一个地弹出,但会阻止已加载块上的任何活动或用户输入。因此,整体渐进水合实施的要求如下。
允许对所有组件使用 SSR
支持将代码拆分为单独的组件或块
支持客户端按照开发人员定义的顺序对这些块进行水合作用
不会阻止用户对已经水合的块进行输入
允许对延迟水合的块使用某种加载指示器
React 并发模式可满足这些要求。它允许 React 同时处理不同的任务,并根据给定的优先级在它们之间切换。切换时,不需要提交部分渲染的树,这样一旦 React 切换回同一任务,渲染任务就可以继续。
并发模式可用于实现渐进水合。在这种情况下,页面上每个块的水合就成为 React 并发模式的一项任务。如果需要执行更高优先级的任务(例如用户输入),React 将暂停水合任务并切换到接受用户输入。 lazy()、Suspense() 等功能允许您使用声明式加载状态。这些可用于在延迟加载时显示加载指示器。 SuspenseList() 可用于定义延迟加载组件的优先级。 Dan Abramov 分享的这个演示展示了并发模式的实际应用,并实现了渐进式补水。
React 并发模式还可以与另一个 React 功能结合使用
- 服务器组件 —— 这将允许您从服务器重新获取组件并在它们流入时将它们渲染在客户端上,而不是等待整个获取完成。因此,即使我们等待网络获取完成,客户端的 CPU 也会开始工作。
虽然基于 React 并发模式的渐进式水合实现仍在准备中,但部分水合实现的许多其他竞争者已经可用。渐进式水合作用已在 Google I/O ‘19 上得到展示。渐进式水合演示展示了如何使用 Hydrator 组件来水合页面的选定部分。由此产生了针对不同客户端框架的多种实现。还可以用于 Vue、Angular 和 Next.js 的实现。
让我们快速浏览一下使用 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]);
组件 HydrationData
用于将序列化的 props 写入客户端。它将确保被水合的组件可以获得所需的道具。
import Header from "../../components/header";
import Main from "../../components/main";
import { HydrationData } from "next-super-performance";
export default function Home() {
return (
<section>
<Header />
<Main />
<HydrationData />
</section>
);
}
优点和缺点
渐进式水合提供了服务器端渲染和客户端水合,同时还最大限度地降低了水合成本。以下是由此可以获得的一些优势。
代码分割:代码分割是构成渐进式水合的一个部分,因为需要为延迟加载的各个组件创建代码块。
允许按需加载页面中不常用的部分:页面的某些组件可能大部分是静态的、在视口之外和/或不经常需要的此类组件是延迟加载的理想选择。页面加载时不需要发送这些组件的水合代码。相反,它们可能会根据触发因素而水合。
减少捆绑包大小:代码分割会自动减少捆绑包大小。加载时执行的代码较少有助于缩短 FCP 和 TTI 之间的时间。
不利的一面是,渐进式水合作用可能不适合动态应用程序,因为屏幕上的每个元素都可供用户使用,并且需要在加载时进行交互。这是因为,如果开发人员不知道用户可能首先点击哪里,他们可能无法确定首先要水合哪些组件。
End.