渐进补水 ✔

简介

服务器呈现的应用程序使用服务器为当前导航生成 HTML。
一旦服务器完成生成 HTML 内容(其中还包含正确显示静态 UI 所需的 CSS 和 JSON 数据),它就会将数据发送给客户端。
由于服务器为我们生成了标记,客户端可以快速解析它并将其显示在屏幕上,从而产生快速的 First Contentful Paint!

尽管服务器渲染提供了更快的首次内容绘制,但它并不总是提供更快的交互时间。
与我们的网站进行交互所需的 JavaScript 尚未加载。按钮可能看起来是交互式的,但它们还不是交互式的(目前)。
只有在 JavaScript 包被加载和处理后,处理程序才会被附加。这个过程称为 hydration:React 检查当前的 DOM 节点,并使用相应的 JavaScript 来对节点进行 hydration。

用户在屏幕上看到非交互式 UI 的时间也被称为恐怖谷:虽然用户可能认为他们可以与网站进行交互,但还没有附加到组件的处理程序。
这对用户来说可能是一种令人沮丧的体验,因为 UI 可能会像被冻结一样!

从服务器接收到的 DOM 组件可能需要一段时间才能完全水合。
在组件可以被水化之前,需要加载、处理和执行 JavaScript 文件。不像我们之前那样一次性对整个应用程序加水,我们还可以逐步对 DOM 节点加水。
渐进式水合使得随着时间的推移单独水合节点成为可能,这使得仅请求最少必要的 JavaScript 成为可能。

通过逐步补水应用程序,我们可以延迟页面不太重要的部分的补水。

这样,我们可以减少为了使页面具有交互性而必须请求的 JavaScript 量,并且仅在用户需要时才对节点进行水合。

渐进式补水还有助于避免最常见的 SSR 补水陷阱,即服务器渲染的 DOM 树被破坏然后立即重建。

渐进式水化允许我们仅根据特定条件对组件进行水化,例如当组件在视口中可见时。

在下面的示例中,我们有一个用户列表,一旦列表出现在视口中,这些用户列表就会逐渐变水。

当组件已被水合时,紫色闪烁显示!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

//App.js
import React from "react";
import { Hydrator as ClientHydrator, ServerHydrator } from "./Hydrator";

let load = () => import("./Stream");
let Hydrator = ClientHydrator;

if (typeof window === "undefined") {
  Hydrator = ServerHydrator;
  load = () => require("./Stream");
}

export default function App() {
  return (
    <div id="app">
      <div className="intro">
        <p>
          This is an example of how server-side rendered React can enable{" "}
          <strong>progressively hydrated</strong> experiences.
        </p>
        <p>
          <strong>Scroll down.</strong> The flash of color you see is an
          indicator of JavaScript being fetched without any direct change to the
          UI.
        </p>
      </div>
      <Hydrator load={load} />
    </div>
  );
}

//client.js
import React from "react";
import { hydrate } from "react-dom";
import App from "./components/App";

hydrate(<App />, document.getElementById("root"));

//server.js
import React from "react";
import { renderToNodeStream } from "react-dom/server";
import App from "./components/App";

export default async () => renderToNodeStream(<App />);

虽然它发生得很快,但您可以看到初始 UI 与处于水合状态的 UI 相同!

由于最初的 HTML 包含相同的信息和样式,我们可以无缝地使组件具有交互性,而无需任何华丽或跳跃的 UI。

渐进式水化可以有条件地使某些组件具有交互性,而您的应用程序用户可能完全不会注意到这一点。


渐进式水化实施

在使用 React 实现 SSR 的部分中,我们讨论了在服务器上呈现的应用程序的客户端水化。

Hydration 允许客户端 React 识别在服务器上呈现的 ReactDOM 组件并将事件附加到这些组件。
因此,一旦 SSR 应用程序在客户端上可用,它就会为 SSR 应用程序引入连续性和无缝性,使其像 CSR 应用程序一样运行。

为了让页面上的所有组件通过水化成为可交互的,这些组件的 React 代码应该包含在下载到客户端的包中。
主要由 JavaScript 控制的高度交互的 SPA 需要一次性完成整个包。

但是,大多数静态网站在屏幕上具有一些交互式元素,可能不需要所有组件立即处于活动状态。
对于这样的网站,为屏幕上的每个组件发送一个巨大的 React 包成为一种开销。

Progressive Hydration 通过允许我们在页面加载时仅对应用程序的某些部分进行水合来解决这个问题。
其他部分根据需要逐渐水合。

通过渐进式水合,“您可能还喜欢”和“其他内容”组件可以稍后进行水合。

水化步骤不是立即初始化整个应用程序,而是从 DOM 树的根部开始,但是服务器渲染的应用程序的各个部分会在一段时间内被激活。
水合过程可能会因各种分支而停止,并在它们进入视口时或基于某些其他触发器后恢复。
请注意,执行每个 hydration 所需的资源加载也使用代码拆分技术延迟,从而减少了使页面具有交互性所需的 JavaScript 量。

渐进式水化背后的想法是通过分块激活您的应用程序来提供出色的性能。
任何渐进式补水解决方案还应考虑它将如何影响整体用户体验。
不能让屏幕块一个接一个地弹出,但阻止已经加载的块上的任何活动或用户输入。

因此,整体渐进式水化实施的要求如下。

  1. 允许对所有组件使用 SSR

  2. 支持将代码拆分为单独的组件或块

  3. 支持在开发人员定义的序列中对这些块进行客户端水合

  4. 不会阻止用户对已经水合的块进行输入

  5. 允许对延迟水化的块使用某种加载指示器

一旦所有人都可以使用 React 并发模式,它将满足所有这些要求。

它允许 React 同时处理不同的任务,并根据给定的优先级在它们之间切换。
切换时,部分渲染的树不需要提交,这样一旦 React 切换回相同的任务,渲染任务就可以继续。

并发模式可用于实现渐进式水化
在这种情况下,页面上每个块的 hydration 成为 React 并发模式的任务。
如果需要执行用户输入等更高优先级的任务,React 将暂停水化任务并切换到接受用户输入。
诸如lazy()、Suspense() 等功能允许您使用声明式加载状态。这些可用于在块被延迟加载时显示加载指示器。 SuspenseList() 可用于定义延迟加载组件的优先级。

这个演示展示了并发模式的作用并实现了渐进式水化。

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 对客户端上的其余组件进行水合,该 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>
 );
}

渐进水合的优缺点

渐进式水化提供服务器端渲染和客户端水化,同时还最大限度地降低了水化成本。

以下是可以从中获得的一些优势。

  1. 促进代码拆分:
    代码拆分是渐进式水化的一个组成部分,因为需要为延迟加载的单个组件创建代码块。

  2. 允许按需加载页面的不常用部分:
    页面的某些组件可能大部分是静态的、在视口之外和/或不需要经常使用。 这些组件是延迟加载的理想选择。 页面加载时不需要发送这些组件的水化代码。 相反,它们可能会根据触发条件进行水合。

  3. 减少包大小:
    代码拆分自动导致包大小的减少。 加载时执行的代码更少,有助于缩短 FCP 和 TTI 之间的时间。

不利的一面是,渐进式补水可能不适合动态应用程序,其中屏幕上的每个元素都可供用户使用,并且需要在加载时进行交互。
这是因为,如果开发人员不知道用户可能首先点击哪里,他们可能无法确定首先要水合的组件。


知识点

  • Hydration

  • React 并发模式

  • next-super-performance - hydrate HydrationData