渲染模式
渐进式水合
服务器渲染的应用程序使用服务器生成当前导航的 HTML。 一旦服务器完成生成 HTML 内容(也包含显示静态 UI 所需的 CSS 和 JSON 数据),它就会将数据发送到客户端。 由于服务器为我们生成了标记,因此客户端可以快速解析它并在屏幕上显示它,从而产生快速的首屏渲染!
虽然服务器渲染提供了更快的首屏渲染,但它并不总是提供更快的可交互时间。 与我们的网站交互所需的必要 JavaScript 尚未加载。 按钮可能看起来可交互,但它们不可交互(尚未)。 处理程序只有在 JavaScript 包加载并处理后才会附加。 此过程称为水合:React 检查当前 DOM 节点,并使用相应的 JavaScript 使节点水合。
用户在屏幕上看到非交互式 UI 的时间也被称为恐怖谷:虽然用户可能认为他们可以与网站交互,但组件上还没有附加任何处理程序。 对于用户来说,这可能是一种令人沮丧的体验,因为 UI 可能看起来像是冻结的!
从服务器接收的 DOM 组件完全水合可能需要一段时间。 在组件可以水合之前,需要加载、处理和执行 JavaScript 文件。 我们也可以渐进地水合 DOM 节点,而不是像以前那样一次性水合整个应用程序。 渐进式水合使随着时间的推移单独水合节点成为可能,这使得只请求最必要的 JavaScript 成为可能。
通过渐进式水合应用程序,我们可以延迟页面不太重要的部分的水合。 这样,我们可以减少为了使页面交互而必须请求的 JavaScript 数量,并且只在用户需要时才水合节点。 渐进式水合也有助于避免最常见的 SSR 水合陷阱,其中服务器渲染的 DOM 树被销毁,然后立即重建。
渐进式水合允许我们仅根据特定条件(例如组件在视窗中可见时)来水合组件。 在以下示例中,我们有一个用户列表,一旦列表进入视窗就会渐进地水合。 紫色闪烁显示组件已被水合!
1import React from "react";2import { hydrate } from "react-dom";3import App from "./components/App";45hydrate(<App />, document.getElementById("root"));
虽然它发生得很快,但您可以看到初始 UI 与其水合状态下的 UI 相同! 由于初始 HTML 包含相同的信息和样式,因此我们可以无缝地使组件交互,没有任何华丽或跳跃的 UI。 渐进式水合使得有条件地使某些组件交互成为可能,而这对于您的应用程序用户来说可能是完全没有注意到的。
渐进式水合实现
在使用 React 实现 SSR 的部分中,我们讨论了在服务器上渲染的应用程序的客户端水合。 水合允许客户端 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
组件用于将序列化道具写入客户端。 它将确保所需道具可用于正在水合的组件。
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 之间的时间。
另一方面,渐进式水合可能不适合动态应用程序,在这些应用程序中,屏幕上的每个元素都可供用户使用,并且需要在加载时变得交互式。 这是因为,如果开发人员不知道用户最有可能点击哪里,他们可能无法确定要首先水合哪些组件。