什么是部分水合?为什么大家都在谈论它?

2025-06-07

什么是部分水合?为什么大家都在谈论它?

在《客户端 Rehydration 的成本》(2019 年 2 月 8 日),Addy Osmani 指出:

服务器渲染页面,然后通过 UI 依赖项的序列化版本在客户端重新水化 DOM(rehydration)可能会带来实际成本。这会严重延迟可交互时间,因为 UI 在客户端处理完成之前就已经看起来准备就绪了。

他想知道,提前绘制像素会给用户带来“恐怖谷效应”,而补液带来的益处是否会被抵消。为了说明这一点,他绘制了以下图表:

01-补液-恐怖谷

大纲

补水还是不补水

Hydration(或 Rehydration)是一种使用客户端 JavaScript 将静态 HTML 页面转换为动态网页的技术,方法是将事件处理程序附加到 HTML 元素。JavaScript 可以通过静态托管或服务器端渲染来交付。

部分补液和渐进补液

部分 rehydration 是渐进式 rehydration 理念的延伸,其中服务器渲染应用程序的各个部分会随着时间的推移逐渐“启动”。这与一次性初始化整个应用程序的方法不同。为了系统地定义和比较各种常用的渲染和 rehydration 技术,Addy Osmani 和 Jason Miller 于2019 年 2 月 6 日发表了《Web 渲染》(Rendering on the Web)

事实证明,部分补液难以实现。这种方法是渐进式补液理念的延伸,即分析需要渐进式补液的各个部分(组件/视图/树),并识别出那些交互性较低或没有反应性的部分。

对于这些大部分静态的部分,相应的 JavaScript 代码将被转换为惰性引用和装饰功能,从而将其客户端占用空间减少到接近零。

听起来很棒!但就像科技界所有东西一样,没有什么灵丹妙药(尤其灵丹妙药只对付狼人式的问题有效)。

部分水合方法本身也存在问题和妥协。它给缓存带来了一些有趣的挑战,而客户端导航意味着我们不能假设应用程序惰性部分的服务器渲染 HTML 在没有完整页面加载的情况下可用。

为了总结文章的观点,作者们提供了这张庞大的比较表:

02 不同渲染技术的比较图

但这张表并不是故事的全部,正如 Jason Miller 在Twitter上指出的那样:

我想澄清一件事:这条推文中的图表并没有说明全部情况,也没有显示应用了组件缓存等可能的优化的 SSR+Hydration。

为了扩展这一观点,Mike Sherov认为:

该图表讨论了相对于首次内容绘制的首次字节时间和可交互时间,但没有讨论相对于首次内容绘制的首次字节时间……因此,它忽略了服务器端渲染解决方案渲染速度更快这一积极事实。该图表假设“带(re)hydration 的服务器端渲染”不采用“JavaScript 渐进增强”……也就是说,它假设页面只有在所有 JavaScript 渲染完成后才能正常运行。

Next.js 和其他一些工具鼓励 JavaScript 进行渐进式增强,因此可交互时间 (TCR) 仍然等于首次内容绘制时间 (FPC)。正如免责声明推文中所述,这忽略了 SSR HTML 的 CDN 缓存,但忽略它极大地改变了“使用 JavaScript 进行 SSR 渐进式增强”的价值主张。HTML 的 CDN 缓存灵活且具有较高的首次字节时间 (TCR)。

互动岛

在继续探索这个问题空间的过程中,Jason Miller 于2020 年 8 月 11 日创造了“岛式架构”一词,该术语指的是 Katie Sylor-Miller 倡导的“组件岛”模式。他认为,Web 开发者应该致力于创建“交互岛”,并精心挑选 JavaScript 代码,只在必要时才添加。

在“孤岛”模型中,服务器渲染并非旨在提升 SEO 或用户体验的附加优化。相反,它是页面如何传递到浏览器的一个基本组成部分。导航响应中返回的 HTML 包含用户请求内容的有意义且可立即渲染的表示。

这是否意味着部分水合和岛屿架构可以互换,或者部分水合是岛屿架构的一种实现?并非完全如此。根据 Ryan Carniato 在《你的未来会是 0kb JavaScript 吗?》(2021 年 5 月 3 日)中的说法,部分水合与岛屿架构非常相似,因为最终结果都是交互岛屿,但不同之处在于创作体验。

您无需编写静态层和 Island 层,而是像编写单个应用一样编写代码,更像一个典型的前端框架。部分水合 (Partial Hyption) 可以自动创建 Island,以便您将最精简的代码发送到浏览器。Marko 是一个 JavaScript 库,它使用其编译器来自动化此部分水合过程,从浏览器包中移除仅服务器渲染的组件。

向现有框架添加部分 Hydration

尽管在2019年和2020年,人们越来越意识到补水的困难以及某种形式的局部补水的必要性,但我们早在很久以前就能找到关于这个话题的讨论。保罗·刘易斯(Paul Lewis)在他的博客文章《当一切都重要时,一切都不重要!》(2016年12月10日)中描述了三种不同的补水水平(他称之为“启动模型”) 。

03-基于 JavaScript 的部分水合服务器渲染+水合物和渐进式渲染+引导程序的三种模式

AngularEmber曾尝试过这种做法。但这些尝试似乎难以获得成功。五年后的今天,Angular 的相关问题仍然悬而未决。Brian Cardarella 在《是否应该使用 Ember FastBoot?》(2017 年 8 月 1 日)一文中指出,DockYard 实施 FastBoot 的成本太高。

反应

2017 年 9 月发布的React v16.0引入了该hydrate()方法作为该方法的替代方案render()。据 Andrew Clark 所述:

React 16 在服务器渲染的 HTML 到达客户端后,能够更好地进行数据同步。它不再要求初始渲染与服务器结果完全匹配。相反,它会尝试尽可能多地复用现有 DOM。无需再进行校验和!

hydrate()行为类似于render()方法。但是,它不是将 React 元素渲染到提供的容器中的 DOM 中并返回对组件的引用,而是hydrate()可以 hydrate 一个渲染 HTML 内容的容器ReactDOMServer,并尝试将事件监听器附加到现有标记。

为了平衡这种权衡,建议开发人员使用 hydrationrender()仅在客户端渲染内容,并hydrate()在服务器端渲染标记之上进行渲染。与 Google 团队一样,React 团队也敦促在决定是否使用 hydration 时要谨慎:

一般来说,我们不建议在客户端和服务器端渲染不同的内容,但在某些情况下这样做可能会有用(例如时间戳)。然而,服务器渲染时缺少节点是很危险的,因为这可能会导致创建具有错误属性的兄弟节点。

两年后,Sebastian Markbåge 发起了一个 PR,将Partial Hydration作为 React 的原生功能实现(2019 年 1 月 28 日):

部分水合 (Partial hydration) 增加了一种机制,可以在页面其他部分仍在加载代码或数据时,对服务器渲染的结果进行部分水合。这意味着,你可以在其他部分仍在水合过程中,开始与屏幕的某些部分进行交互。在此模型中,你始终必须先水合根内容,因为它为子元素提供了 props,而这些 props 的复杂程度可能任意。

该模型假设应用程序的根设计得相对较浅,然后每个抽象随着深度的增加而逐渐变得复杂。为了更快地实现交互,树中的组件本身可以使用渐进增强功能,在初始“hydration”之后增加更多复杂性。

预行动

Markus Oberlehner 在《使用同构 Preact 和 Eleventy 构建部分混合、渐进增强的静态网站》(2020 年 3 月 22 日)中解释了如何将静态站点生成器 Eleventy 与 Preact结合使用,从而预示了 Slinkity(将在后面的部分中详细讨论)

如果我告诉你,我们可以拥有一切,你会怎么想?我们可以使用一个至少与 React 一样强大的现代 JavaScript 框架,并将其与一个出色的静态站点生成器相结合,从而以提供真正的渐进式增强和最小 JavaScript 包大小的方式构建我们的网站。Eleventy 与 Preact 的结合使这一切成为可能。

Vue

Markus Oberlehner基于他将vue-lazy-hydration移植到 Vue 3 的工作,在《部分水合概念:惰性和主动》(2020 年 11 月 8 日)中比较了不同形式的部分水合:

延迟加载是部分加载的一种形式,您可以在稍后触发加载,而不是在网站加载后立即触发。一个很好的例子就是位于视口之外的组件。您不需要立即加载它们,但可以延迟加载,直到组件可见为止。

<template>
  <TheNavigation/>

  <LazyHydrate skip>
    <TheBlogArticle/>
  </LazyHydrate>

  <LazyHydrate when-visible>
    <TheFooter/>
  </LazyHydrate>
</template>
Enter fullscreen mode Exit fullscreen mode

上面提到的延迟加载概念,在我们拥有一个主要以交互为主的应用,但希望将某些部分排除在加载之外时,效果最佳。让我们反过来想象一下:我们有一个包含深层嵌套组件的大型应用,它是一个静态网站,但有一个深层嵌套的组件,它必须是可交互的。

<template>
  <LazyHydrate skip>
    <App/>
  </LazyHydrate>
</template>
Enter fullscreen mode Exit fullscreen mode

坚硬的

Ryan Carniato 受到 Marko(将在后面的部分详细讨论)的影响,在《部分水合》(2020 年 11 月 15 日)中建议使用子组件(或组件级)水合

这是我们 SSR 故事中缺少的最后一个核心功能。说实话,除了 Marko 之外,大多数库在这方面的表现都不太出色。我们也可以先考虑采用更手动的方式。我认为关键的创新在于追随 Marko 的脚步,认识到实际上存在三种部分水合模式,而不是其他库所知道的两种。有一种中间模式可以显著提高我们的效率。鉴于这里使用的细粒度的非组件绑定方法,这是唯一可行的方案。

苗条

根据 Rich Harris 在 2021 年 3 月的说法,部分数据融合 (partial hydration) 已进入 Svelte 的雷达,但尚未列入其路线图。这很合理,因为 Svelte 已经在编译一个不需要运行时的优化的原生 JavaScript 版本。然而,Kevin Åberg Kultalahti 于2021 年 5 月 9 日通过指令提出了Svelte 中的部分数据融合 (Partial Hydration)use:action。我们还将在下一节中看到,一个 Svelte 元框架 Elder.js 已经独立实现了部分数据融合。

为部分水合构建的框架

我们看到,基本上每个主流的前端 JavaScript 框架都尝试添加某种形式的 Partial Hyption 功能,并取得了不同程度的成功。但还有一类完全属于前端的框架,它们从一开始就将 Partial Hyption 视为一项关键功能。

马尔科

如果说有哪个框架可以归功于率先引入部分水合(partial hydration)作为主要功能(甚至在该术语发明之前),我一定会选 Marko。Patrick Steele-Idem 在《异步片段:用 Marko 重新发现渐进式 HTML 渲染》(2014 年 12 月 8 日)一文中详细讨论了 Marko 的内部原理。他还提供了大量现有技术的链接,例如:

Michael Rawlings 在《也许你不需要那个 SPA》(2020 年 5 月 12 日)一文中从过去几年的角度审视了 Marko 的创新

Marko 允许您通过组合组件来构建页面,其中一些组件可以是有状态的。只有那些具有状态或其他针对浏览器逻辑的组件才会真正发送到浏览器,Marko 会自动处理这些组件所需的来自服务器的任何数据的序列化,并将其挂载到浏览器中。

这意味着,对于大多数应用来说,即使进行了代码拆分,最终提交的代码量也比同等 SPA 少得多。如果没有组件需要水合,那什么也不会水合。

Marko 中的 04 全页水合与组件级水合

Elder.js

尽管 Svelte 核心选择在 2020-2021 年期间暂时搁置部分数据融合 (partial hydration) 的开发,但 Nick Reese 开发的 Elder.js 却是早期加入部分数据融合领导地位的先行者之一,它是一个使用 Svelte 构建的静态网站生成器。Elder.js 主要专注于 SEO,它允许你只对客户端中需要交互的部分进行数据融合。

这样,您就可以减少有效负载,同时仍然可以控制组件的延迟加载、预加载和预先加载。虽然 Elder.js 不如 Astro 那么出名,但它早在2020 年8 月就包含了部分水合功能,比 Astro 的 首次提交早了大约六个月

<div class="right">
  <Clock hydrate-client={{}} />
</div>
Enter fullscreen mode Exit fullscreen mode

Astro

尽管 Marko 和 Elder 进行了早期创新,但将部分水合技术推向主流,最值得称赞的框架是 Astro。Fred K. Schott 在《Astro简介:减少 JavaScript 负担》(2021 年 6 月 8 日)中介绍了 Astro 的架构和目标。

Astro 的工作原理很像一个静态网站生成器。如果你曾经使用过 Eleventy、Hugo 或 Jekyll(或者像 Rails、Laravel 或 Django 这样的服务器端 Web 框架),那么你应该对 Astro 感到很熟悉。

在 Astro 中,您可以使用您常用的 JavaScript Web 框架(React、Svelte、Vue 等)中的 UI 组件来构建网站。Astro 在构建过程中会将您的整个网站渲染为静态 HTML。最终结果是完全静态的网站,并且所有 JavaScript 代码都会从最终页面中删除。

目前已有许多基于ReactVueSvelte的框架,它们都具备在构建时将组件渲染为静态 HTML 的功能。但是,如果您想在客户端上整合这些项目,则必须将一整套依赖项与静态 HTML 一起打包。与这些之前的框架不同,Astro 能够在需要时仅加载单个组件及其依赖项。

当然,有时客户端 JavaScript 不可避免地会出现在图片轮播、购物车和自动完成搜索栏等功能中。Astro 的真正优势就在于此:当某个组件需要 JavaScript 时,Astro 只会加载该组件(以及所有依赖项)。网站的其余部分将继续以静态、轻量级的 HTML 形式存在。

在 Astro 中,这种部分水合 (partial hydration) 功能内置于工具本身。你甚至可以使用 :visible 修饰符自动延迟组件加载,使其仅在页面可见时才加载。这种新的 Web 架构方法被称为“孤岛架构”。

Astro 包含五个client:*指令,用于在运行时在客户端上对组件进行“混合”。指令是一个组件属性,用于告诉 Astro 应该如何渲染组件。

  • <MyComponent client:load />- 在页面加载时补充组件。
  • <MyComponent client:idle />- 主线程空闲后立即为组件补充水分。
  • <MyComponent client:visible />- 元素进入视口后立即对组件进行 Hypnotize。这对于页面下方的内容非常有用。
  • <MyComponent client:media={QUERY} />- 一旦浏览器匹配指定的媒体查询,就立即对组件进行 hydration。这对于侧边栏切换按钮或其他仅应在移动设备或桌面设备上显示的元素非常有用。
  • <MyComponent client:only />- 在页面加载时对组件进行水合操作,类似于client:load。该组件将在构建时被跳过,这对于完全依赖于客户端 API 的组件非常有用。除非绝对必要,否则最好避免使用。

MyReactComponent下面的示例使用在浏览器中对React 组件()进行补充client:visibleclient:visible意味着该组件在用户浏览器中可见之前不会加载任何客户端 JavaScript。

---
import MyReactComponent from '../components/MyReactComponent.jsx';
---

<MyReactComponent client:visible />
Enter fullscreen mode Exit fullscreen mode

斯林奇蒂

Slinkity是一个使用 Vite 的框架,它通过部分数据迁移 (partial hydration)为静态的 11ty 网站带来动态的客户端交互。在《使用 Vite + Partial Hydration 将 JavaScript 交付到需要的地方》(Ship JavaScript where it counts with Vite + Partial Hydration) (2021 年 11 月 12 日)中,Ben Holmes 提出了默认关闭部分数据迁移的方案,以便开发者必须明确选择加入:

目前,Jamstack 的格局绝对依赖于一种“选择退出”的心态。初始页面加载时 JavaScript 过多?可以通过代码拆分和 ESM 延迟加载来选择退出。公司启动页需要减少 JavaScript 数量?可以通过服务器渲染组件来选择退出。Astro、Slinkity + 11ty 或 Îles 引入的部分水合 (partial hydration) 技术,将这种“选择退出”转变为“选择加入”。

初始页面加载时 JavaScript 太多?好吧,你需要为你的 UI 组件启用 JavaScript 混合功能,否则就会出现这个问题!这些框架默认不为你的 React、Vue、Svelte 等组件加载 JavaScript,而是使用混合“模式”来决定如何以及何时加载这些资源(如果有的话)。

要选择如何呈现给定的组件,您需要传递一个renderprop,就像字面上名为 的 prop 一样render

export const frontMatter = {
  render: 'eager';
}

function Page() {...}
Enter fullscreen mode Exit fullscreen mode

render或者,您可以使用包含您想要选择的 Hydration 选项的短代码。所有组件的默认操作均eager沿用之前基于组件的框架的操作方式。

<!-- page-with-shortcode.html -->

<body>
  {% react 'components/Example' 'render' 'eager' %}
</body>
Enter fullscreen mode Exit fullscreen mode
  • 加载的组件eager将被渲染为静态 HTML,并作为 JavaScript 包发送到客户端。在前面的示例中,访问会在页面解析完成后立即page-with-shortcode.html导入 React 和components/Example.jsxJavaScript 包,以确保我们的组件尽快实现交互。
  • lazy类似于,不同之处在于它仅当您的组件通过使用Intersection Observer APIeager滚动到视图中时才加载组件的 JavaScript
  • static组件在构建时渲染为 HTML,不会将任何 JavaScript 发送到客户端,这意味着没有交互性或状态管理。如果你想将 React 等组件语言仅用作模板语言,这将非常有用。

岛屿

îles是一个提供自动部分水合功能的静态站点生成器。该项目的名称在法语中意为“岛屿”,巧妙地致敬了岛屿架构及其构建工具 Vite。受 Astro 的启发,您可以使用client:组件中的指令来定义哪些组件应在生产构建中保持交互。以下是使用 MDX 的示例:

---
audio: /song-for-you.mp3
---

## Play a song

<AudioPlayer {...frontmatter} client:visible/>
Enter fullscreen mode Exit fullscreen mode

目录中的组件src/components会按需自动导入,因此上面的示例无需导入AudioPlayer组件。您还可以在 Vue 组件内部使用指令,如下例所示:

<Audio client:visible :src="audio" :initialDuration="initialDuration"/>
Enter fullscreen mode Exit fullscreen mode

Qwik

Qwik专注于 HTML 服务器端渲染的可恢复性和代码的细粒度延迟加载。它旨在实现最佳的交互时间。Miško Hevery 在《初探 Qwik - HTML 优先框架》(2021 年 6 月 23 日)中指出:

Qwik 的基本目标是通过尽可能延迟 JavaScript 的执行来充分利用浏览器的延迟加载功能,从而专注于可交互时间指标。这与现有框架形成了鲜明对比,这些框架更多地将服务器端渲染和可交互时间视为事后诸葛亮,而非驱动所有其他设计决策的主要目标。

有了 Qwik,我们现在有了一个明确旨在优先考虑可交互时间的框架,它通过可恢复性来实现这一点。但可恢复性究竟意味着什么呢?

Qwik 背后的基本理念是可续传。它可以从服务器中断的地方继续执行。客户端只需执行极少量的代码。qwikloader它接收服务器端渲染生成的静态 HTML 并继续执行,其大小不到 1kb,执行时间不到 1ms……您网站的所有其他交互内容都会在您以尽可能小的块与网站交互时以惰性方式下载。

Qwik 还会异步且无序地补充组件,以确保第一次交互不会导致完整的应用程序下载和引导。

这里的异步意味着渲染系统可以暂停渲染,异步下载组件的模板,然后继续渲染过程。这与所有拥有完全同步渲染管线的现有框架形成了鲜明对比。而且由于渲染是同步的,因此没有地方可以插入异步延迟加载。结果是所有模板都需要在调用渲染之前就已存在。

Ryan Carniato 对 Qwik 有如下评价:

它是唯一一个能够从只加载页面所需的 JavaScript(甚至可能从 0kb 开始)到能够实现完整 SPA 的框架。它实际上可以通过无序惰性加载 (lazy hydration) 的方式,在加载完成后引入客户端路由。

据我所知,它是目前唯一一个基本上以“岛”形式运行,但可以根据需要变形成单个应用程序的框架。Marko 和其他公司也即将推出此功能,但我们不应该操之过急。而且,对于传统的 SPA 来说,这还有很长的路要走。

结论

像 Qwik 这样的框架让我们完全理解了 Addy Osmani 对开发者的警告:降低交互时间分数可能会导致恐怖谷效应。网站提供的 HTML 框架看起来应该是可交互的,但实际上却暂时无法交互,因为用户必须等待客户端完成加载。

05-qwik-交互图形时间

但正如本文所示,一旦开发人员能够配置这些参数,他们就会发现自己陷入了一个复杂的权衡决策矩阵。下一步的发展将是利用这些性能优势,并设计一些约定来简化这些技术,并在开发人员已经习惯使用的框架中创建令人满意的路径。

特别感谢 Ryan Carniato 对这篇文章的早期反馈,以及 Ben Holmes 与我就这些事情进行了数月的讨论。

文章来源:https://dev.to/ajcwebdev/what-is-partial-Hydration-and-why-is-everyone-talking-about-it-3k56
PREV
JavaScript 构造函数入门
NEXT
AWS 无服务器速成课程 - 使用 Lambda 和 S3 动态调整图像大小