progressive-hydration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseProgressive Hydration
渐进式水合(Progressive Hydration)
A server rendered application uses the server to generate the HTML for the current navigation. Once the server has completed generating the HTML contents, which also contains the necessary CSS and JSON data to display the static UI correctly, it sends the data down to the client. Since the server generated the markup for us, the client can quickly parse this and display it on the screen, which produces a fast First Contentful Paint!
Although server rendering provides a faster First Contentful Paint, it doesn't always provide a faster Time To Interactive. The necessary JavaScript in order to be able to interact with our website hasn't been loaded yet. Buttons may look interactive, but they aren't interactive (yet). The handlers will only get attached once the JavaScript bundle has been loaded and processed. This process is called hydration: React checks the current DOM nodes, and hydrates the nodes with the corresponding JavaScript.
服务器端渲染(SSR)应用会通过服务器为当前导航生成HTML。服务器生成包含正确显示静态UI所需的CSS和JSON数据的HTML内容后,会将数据发送给客户端。由于标记(markup)由服务器生成,客户端可以快速解析并在屏幕上显示,从而实现更快的首次内容绘制(First Contentful Paint)!
尽管服务器端渲染能提供更快的首次内容绘制,但并不总能带来更快的可交互时间。与网站交互所需的JavaScript尚未加载完成,按钮看起来是可交互的,但实际上还无法响应操作。只有当JavaScript包加载并处理完成后,事件处理器才会被绑定到元素上。这个过程被称为水合(hydration):React会检查当前DOM节点,并为对应的节点绑定JavaScript逻辑。
When to Use
适用场景
- Use this when your SSR application has non-critical sections that don't need immediate interactivity
- This is helpful for reducing the JavaScript required to make the page interactive on initial load
- 当你的SSR应用存在无需立即交互的非关键区域时使用
- 有助于减少初始加载时让页面具备可交互性所需的JavaScript体积
Instructions
实现步骤
- Wrap non-critical components in boundaries with appropriate fallbacks
<Suspense> - Use with code-splitting to defer loading of below-the-fold or rarely-used components
React.lazy() - In React 18+, leverage selective hydration which automatically prioritizes user-interacted areas
- Use with
next/dynamicin Next.js for truly client-only widgets{ ssr: false }
- 用边界包裹非关键组件,并设置合适的回退内容
<Suspense> - 结合与代码分割,延迟加载折叠区域以下或极少使用的组件
React.lazy() - 在React 18+中,利用选择性水合功能,它会自动优先处理用户交互的区域
- 在Next.js中,使用并配置
next/dynamic来实现纯客户端组件{ ssr: false }
Details
详细说明
The time that the user sees non-interactive UI on the screen is also referred to as the uncanny valley: although users may think that they can interact with the website, there are no handlers attached to the components yet. This can be a frustrating experience for the user, as the UI may look like it's frozen!
It can take a while before the DOM components that were received from the server are fully hydrated. Before the components can be hydrated, the JavaScript file needs to be loaded, processed, and executed. Instead of hydrating the entire application at once, like we did previously, we can also progressively hydrate the DOM nodes. Progressive hydration makes it possible to individually hydrate nodes over time, which makes it possible to only request the minimum necessary JavaScript.
By progressively hydrating the application, we can delay the hydration of less important parts of the page. This way, we can reduce the amount of JavaScript we have to request in order to make the page interactive, and only hydrate the nodes once the user needs it. Progressive hydration also helps avoid the most common SSR Rehydration pitfalls where a server-rendered DOM tree gets destroyed and then immediately rebuilt.
Progressive hydration allows us to only hydrate components based on a certain condition, for example when a component is visible in the viewport.
用户看到非交互UI的这段时间也被称为“恐怖谷”:尽管用户认为可以与网站交互,但组件尚未绑定任何事件处理器,这会给用户带来糟糕的体验,UI看起来像是“冻结”了一样!
从服务器接收的DOM组件可能需要一段时间才能完全完成水合。在组件水合之前,JavaScript文件需要先加载、处理并执行。我们不必像之前那样一次性水合整个应用,而是可以渐进式地水合DOM节点。渐进式水合允许我们随时间逐个水合节点,这样就只需要请求必要的最小体积JavaScript。
通过渐进式水合应用,我们可以延迟页面非关键部分的水合。这样一来,我们就能减少让页面具备可交互性所需请求的JavaScript量,仅在用户需要时再水合对应节点。渐进式水合还有助于避免常见的SSR水合陷阱——即服务器渲染的DOM树被销毁后立即重建的问题。
渐进式水合允许我们根据特定条件水合组件,例如当组件进入视口时。
Progressive Hydration Implementation
渐进式水合的实现
In the section on implementing SSR with React, we discussed client-side hydration for an app that is rendered on the server. Hydration allows client-side React to recognize the ReactDOM components that are rendered on the server and attach events to these components. Thus, it introduces continuity and seamlessness for an SSR app to function like a CSR app once it is available on the client.
For all components on the page to become interactive via hydration, the React code for these components should be included in the bundle that gets downloaded to the client. Highly interactive SPAs that are largely controlled by JavaScript would need the entire bundle at once. However, mostly static websites with a few interactive elements on the screen, may not need all components to be active immediately. For such websites sending a huge React bundle for each component on the screen becomes an overhead.
Progressive Hydration solves this problem by allowing us to hydrate only certain parts of the application when the page loads. The other parts are hydrated progressively as required.
Instead of initializing the entire application at once, the hydration step begins at the root of the DOM tree, but the individual pieces of the server-rendered application are activated over a period of time. The hydration process may be arrested for various branches and resumed later when they enter the viewport or based on some other trigger. Note that, the loading of resources required to perform each hydration is also deferred using code-splitting techniques, thereby reducing the amount of JavaScript required to make pages interactive.
The idea behind progressive hydration is to provide a great performance by activating your app in chunks. Any progressive hydration solution should also take into account how it will impact the overall user experience. The requirements for a holistic progressive hydration implementation are as follows:
- Allows usage of SSR for all components.
- Supports splitting of code into individual components or chunks.
- Supports client side hydration of these chunks in a developer defined sequence.
- Does not block user input on chunks that are already hydrated.
- Allows usage of some sort of a loading indicator for chunks with deferred hydration.
React concurrent mode will address all these requirements once it is available to all. It allows React to work on different tasks at the same time and switch between them based on the given priority. When switching, a partially rendered tree need not be committed, so that the rendering task can continue once React switches back to the same task.
Concurrent mode can be used to implement progressive hydration. In this case, hydration of each of the chunks on the page, becomes a task for React concurrent mode. If a task of higher priority like user input needs to be performed, React will pause the hydration task and switch to accepting the user input. Features like , allow you to use declarative loading states. These can be used to show the loading indicator while chunks are being lazy loaded. can be used to define the priority for lazy loading components.
lazy()Suspense()SuspenseList()React concurrent mode can also be combined with another React feature:
- Server Components. This will allow you to refetch components from the server and render them on the client as they stream in instead of waiting for the whole fetch to finish. Thus, the client's CPU is put to work even as we wait for the network fetch to finish.
Multiple implementations of progressive hydration are available. A POC for partial hydration using Preact and Next.js uses:
- : A library that implements partial hydration with Preact x.
pool-attendant-preact - : A Next.js plugin that uses this library to improve client-side performance.
next-super-performance
The library includes an API called which lets you mark your more interactive components for hydration. These will be hydrated first:
pool-attendant-preactwithHydrationjs
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>
);
}The component in columns 2 and 3 will be hydrated first. You can now hydrate the remaining components on the client using the API:
HydratedTeaserhydrate()js
import { hydrate } from "next-super-performance";
import Teaser from "./components/teaser";
hydrate([Teaser]);在讨论如何用React实现SSR的章节中,我们提到了客户端水合的概念:它让客户端React识别服务器渲染的ReactDOM组件,并为这些组件绑定事件。因此,它能让SSR应用在客户端可用后,无缝过渡得像CSR应用一样运行。
要让页面上的所有组件通过水合变得可交互,这些组件的React代码需要包含在下载到客户端的包中。高度交互、主要由JavaScript控制的SPA需要一次性加载整个包。但对于大部分是静态内容、只有少量交互元素的网站来说,可能不需要所有组件立即激活。这类网站如果为屏幕上的每个组件都发送庞大的React包,会造成不必要的开销。
渐进式水合解决了这个问题:它允许我们在页面加载时只水合应用的特定部分,其他部分则根据需要渐进式水合。
不再一次性初始化整个应用,水合步骤从DOM树的根节点开始,但服务器渲染应用的各个部分会在一段时间内逐步激活。水合过程可能会在不同分支暂停,当这些分支进入视口或触发其他条件时再恢复。需要注意的是,执行每次水合所需的资源加载也会通过代码分割技术延迟,从而减少让页面具备可交互性所需的JavaScript体积。
渐进式水合的核心思想是通过分块激活应用来实现出色的性能。任何渐进式水合方案都需要考虑其对整体用户体验的影响。一个完善的渐进式水合实现需要满足以下要求:
- 允许所有组件使用SSR
- 支持将代码拆分为单个组件或代码块
- 支持按照开发者定义的顺序水合这些客户端代码块
- 不会阻塞已水合代码块上的用户输入
- 允许为延迟水合的代码块设置某种加载指示器
React并发模式(Concurrent Mode)一旦全面可用,将满足所有这些要求。它允许React同时处理不同任务,并根据优先级在任务之间切换。切换时,部分渲染的树无需提交,当React切换回该任务时可以继续渲染。
并发模式可用于实现渐进式水合。在这种情况下,页面上每个代码块的水合都会成为React并发模式的一个任务。如果有更高优先级的任务(如用户输入)需要执行,React会暂停水合任务,转而处理用户输入。、等功能允许你声明式地定义加载状态,可用于在懒加载代码块时显示加载指示器。可用于定义懒加载组件的优先级。
lazy()Suspense()SuspenseList()React并发模式还可以与另一个React功能结合使用:
- Server Components。它允许你从服务器重新获取组件,并在组件流式传输到客户端时进行渲染,而无需等待整个请求完成。因此,即使在等待网络请求完成时,客户端的CPU也能保持工作状态。
目前已有多种渐进式水合的实现方案。一个基于Preact和Next.js的部分水合POC使用了以下工具:
- : 一个基于Preact X实现部分水合的库
pool-attendant-preact - : 一个使用该库提升客户端性能的Next.js插件
next-super-performance
pool-attendant-preactwithHydrationjs
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列的组件会被优先水合。你可以使用 API在客户端水合剩余的组件:
HydratedTeaserhydrate()js
import { hydrate } from "next-super-performance";
import Teaser from "./components/teaser";
hydrate([Teaser]);Pros and Cons
优缺点
Progressive hydration provides server-side rendering with client-side hydration while also minimizing the cost of hydration:
-
Promotes code-splitting: Code-splitting is an integral part of progressive hydration because chunks of code need to be created for individual components that are lazy-loaded.
-
Allows on-demand loading for infrequently used parts of the page: There may be components of the page that are mostly static, out of the viewport and/or not required very often. Such components are ideal candidates for lazy loading.
-
Reduces bundle size: Code-splitting automatically results in a reduction of bundle size. Less code to execute on load helps reduce the time between FCP and TTI.
Note (React 18+): Usewith<Suspense>for Hydration Controllazy()React 18's Concurrent Features now address the progressive hydration requirements: selective hydration on interaction means if the user tries to interact with a not-yet-hydrated part, React will prioritize hydrating that part—this is built-in.A practical pattern is to wrap non-critical components in a. On the server, render a lightweight placeholder, and code-split the real component with<Suspense fallback={...}>. For example:React.lazyjsconst Comments = React.lazy(() => import('./Comments')); // ... <Suspense fallback={null}> <Comments {...props} /> </Suspense>This streams a placeholder in the SSR output and doesn't block the rest of the page. Thecomponent hydrates once its code is fetched. In Next.js, useCommentswithnext/dynamicfor truly client-only widgets, or Suspense for parts that can be temporarily left as loading.{ ssr: false }
On the downside, progressive hydration may not be suitable for dynamic apps where every element on the screen is available to the user and needs to be made interactive on load. This is because, if developers do not know where the user is likely to click first, they may not be able to identify which components to hydrate first.
渐进式水合结合了服务器端渲染与客户端水合的优势,同时最大限度降低了水合的成本:
- 促进代码分割:代码分割是渐进式水合不可或缺的一部分,因为需要为懒加载的单个组件创建代码块
- 支持页面低频使用部分的按需加载:页面上可能存在一些基本静态、位于视口外或极少使用的组件,这类组件非常适合懒加载
- 减小包体积:代码分割会自动减小包体积,初始加载时需要执行的代码更少,有助于缩短FCP与TTI之间的时间
注意(React 18+):使用与<Suspense>控制水合lazy()React 18的并发特性现已满足渐进式水合的需求:基于交互的选择性水合意味着,如果用户尝试与尚未水合的部分交互,React会优先水合该区域——这是内置功能。一个实用的模式是用包裹非关键组件。在服务器端渲染轻量级占位符,并通过<Suspense fallback={...}>对真实组件进行代码分割。例如:React.lazy()jsconst Comments = React.lazy(() => import('./Comments')); // ... <Suspense fallback={null}> <Comments {...props} /> </Suspense>这种方式会在SSR输出中流式传输占位符,不会阻塞页面其他部分的渲染。组件会在其代码加载完成后进行水合。在Next.js中,使用Comments并配置next/dynamic实现纯客户端组件,或使用Suspense处理需要临时显示加载状态的部分。{ ssr: false }
缺点方面,渐进式水合可能不适合所有元素都需要在加载时立即具备可交互性的动态应用。因为如果开发者无法预判用户首先会点击哪里,就无法确定优先水合哪些组件。