水合作用纯粹是头顶上方
原文链接:https://www.builder.io/blog/hydration-is-pure-overhead
水合(Hydration)是一种为服务器端渲染的 HTML 添加交互性的解决方案。以下是维基百科对水合的定义:
在 Web 开发中,水合或再水合是一种技术,客户端 JavaScript 通过将事件处理程序附加到 HTML 元素,将静态 HTML 网页(通过静态托管或服务器端渲染提供)转换为动态网页。
上述定义从将事件处理程序附加到静态 HTML 的角度讨论了水合过程。然而,将事件处理程序附加到 DOM 并非水合过程中最具挑战性或最耗费资源的部分,因此它忽略了人们为何会将水合称为一种开销的真正原因。在本文中,开销指的是可以避免且最终结果相同的工作。如果可以移除某项工作且结果相同,那么它就是开销。
深入探究水合作用
水合作用的难点在于了解WHAT我们需要哪些事件处理程序,以及WHERE它们需要如何附加。
WHAT事件处理程序是一个闭包,其中包含了事件处理程序的行为。它定义了当用户触发此事件时应该发生的情况。WHEREWHAT:需要附加到的DOM 元素的位置(包括事件类型)。
更复杂的是,这WHAT是一个包含APP_STATE和 的闭包FRAMEWORK_STATE:
APP_STATE应用程序的状态。APP_STATE这通常是人们理解的状态。如果没有状态APP_STATE,你的应用程序就无法向用户显示任何动态内容。FRAMEWORK_STATE框架的内部状态。如果没有它FRAMEWORK_STATE,框架就不知道要更新哪些 DOM 节点,也不知道何时应该更新它们。例如组件树和渲染函数的引用。
那么我们如何恢复WHAT(APP_STATE+ FRAMEWORK_STATE)和呢WHERE?通过下载并执行当前 HTML 中的组件。下载并执行 HTML 中已渲染的组件是开销最大的部分。
换句话说,水合作用是一种通过在浏览器中快速执行应用程序代码来恢复系统的技巧,具体涉及以下几个方面APP_STATE:FRAMEWORK_STATE
- 下载组件代码
- 执行组件代码
- 恢复
WHAT(APP_STATE和FRAMEWORK_STATE)并WHERE获取事件处理程序闭包 WHAT将(事件处理程序闭包)附加到WHERE(DOM 元素)
我们把前三个步骤称为“RECOVERY阶段”。RECOVERY在这个阶段,框架正在尝试重新构建应用程序。重新构建的开销很大,因为它需要下载并执行应用程序代码。
RECOVERY页面加载时间与页面复杂度成正比,在移动设备上可能需要 10 秒。由于加载RECOVERY是耗费资源的部分,大多数应用程序的启动性能都不理想,尤其是在移动设备上。
RECOVERY这纯粹是开销。开销是指那些不直接产生价值的工作。在水合的上下文中,RECOVERY这属于开销,因为它重建了服务器已经通过服务端渲染/服务端生成收集的信息。这些信息没有发送给客户端,而是被丢弃了。因此,客户端必须执行代价高昂的操作RECOVERY来重建服务器已经拥有的信息。如果服务器将这些信息序列化并与 HTML 一起发送给客户端,RECOVERY就可以避免这种情况。序列化的信息可以避免客户端急于下载和执行 HTML 中的所有组件。
客户端重复执行服务器端已在服务端渲染/服务端生成过程中执行过的代码,这使得水合操作纯粹成为一种开销:也就是说,客户端重复了服务器已经完成的工作。框架本可以通过将信息从服务器传输到客户端来避免这种开销,但它却丢弃了这些信息。
总而言之,水合过程通过下载并重新执行 SSR/SSG 渲染的 HTML 中的所有组件来恢复事件处理程序。网站会两次发送给客户端,一次是 HTML,另一次是 JavaScript。此外,框架必须立即执行 JavaScript 来恢复 `<head>`、`<body>` WHAT、WHERE` APP_STATE<body>` 和 `<body> FRAMEWORK_STATE`。所有这些工作仅仅是为了找回服务器已经拥有但已丢弃的东西!
为了理解为什么水合作用会迫使客户重复工作,让我们来看一个包含几个简单组件的例子。
我们将使用很多人都能理解的流行语法,但请记住,这是一个普遍存在的问题,并非某个特定框架的问题。
export const Main = () => <>
<Greeter />
<Counter value={10}/>
</>
export const Greeter = () => {
return (
<button onClick={() => alert('Hello World!'))}>
Greet
</button>
)
}
export const Counter = (props: { value: number }) => {
const store = useStore({ count: props.number || 0 });
return (
<button onClick={() => store.count++)}>
{count}
</button>
)
}
上述操作在 SSR/SSG 处理后将生成以下 HTML 代码:
<button>Greet</button>
<button>10</button>
HTML 中没有标明事件处理程序或组件边界的位置。生成的 HTML 不包含WHAT`( APP_STATE, FRAMEWORK_STATE)` 或 `<div>`WHERE标签。这些信息在服务器生成 HTML 时就已经存在,但服务器并未将其序列化。客户端要使应用程序具有交互性,唯一的办法就是下载并执行代码来恢复这些信息。我们这样做是为了恢复那些用于关闭状态的事件处理程序闭包。
关键在于,必须先下载并执行代码,然后才能附加任何事件处理程序并处理事件。代码执行会实例化组件并重新创建状态(WHAT(APP_STATE,FRAMEWORK_STATE)和WHERE)。
水合完成后,应用程序即可运行。点击按钮后,用户界面将按预期更新。
恢复能力:一种无需额外成本的水合作用替代方案
那么,如何设计一个无需补水、因而也无需额外开销的系统呢?
为了消除开销,框架不仅必须避免RECOVERY上述步骤,还必须避免第四步。第四步是将组件附加到组件上WHAT,WHERE而这会产生额外的成本,是可以避免的。
为了避免这笔费用,你需要准备三样东西:
- 将所有必需信息序列化为 HTML 的一部分。序列化的信息需要包含 <code><div></code>、<code><span></code>、<code><span></code>
WHAT和WHERE<code>APP_STATE<span></code>FRAMEWORK_STATE。 - 一个全局事件处理程序,它利用事件冒泡机制来拦截所有事件。该事件处理程序必须是全局的,这样我们就无需在特定的 DOM 元素上单独注册所有事件。
- 一个可以延迟恢复事件处理程序(the
WHAT)的工厂函数。
工厂函数是关键!Hydration 会WHAT立即创建组件,因为它需要WHAT将其附加到其他组件上。而我们可以通过在响应用户事件时延迟WHERE创建组件来避免不必要的工作。WHAT
上述设置具有可恢复性,因为它可以从服务器中断的地方继续执行,而无需重复服务器已经完成的工作。更重要的是,该设置没有任何额外开销,因为所有工作都是必要的,并且没有任何工作是重复服务器已经完成的工作。
理解二者区别的一个好方法是观察推式系统和拉式系统。
- 推送(水合):积极下载并执行代码,以便在用户交互时积极注册事件处理程序。
- 拉取(可恢复性):什么也不做,等待用户触发事件,然后延迟创建处理程序来处理该事件。
在水合机制中,事件处理程序的创建发生在事件触发之前,因此是急切型的。水合机制还要求创建并注册所有可能的事件处理程序,以防用户触发事件(这可能是不必要的工作)。因此,事件处理程序的创建是推测性的,是可能不需要的额外工作。(此外,事件处理程序的创建是通过重复服务器已经完成的工作来实现的;因此,这是一种开销。)
在可恢复系统中,事件处理程序的创建是延迟执行的。因此,事件处理程序的创建发生在事件触发之后,并且严格按照需要进行。框架通过反序列化事件来创建事件处理程序,因此客户端不会重复服务器已经完成的工作。
Qwik的工作原理是延迟创建事件处理程序,这使得它能够实现快速的应用程序启动时间。
可恢复性要求我们对WHAT( APP_STATE, FRAMEWORK_STATE) 和进行序列化WHERE。一个可恢复的系统可能会生成以下 HTML 代码作为存储WHAT( APP_STATE, FRAMEWORK_STATE) 和 的可能解决方案WHERE。具体细节并不重要,重要的是所有信息都存在。
<div q:host>
<div q:host>
<button on:click="./chunk-a.js#greet">Greet</button>
</div>
<div q:host>
<button q:obj="1" on:click="./chunk-b.js#count[0]">10</button>
</div>
</div>
<script>/* code that sets up global listeners */</script>
<script type="text/qwik">/* JSON representing APP_STATE, FRAMEWORK_STATE */</script>
当上述 HTML 代码在浏览器中加载时,它会立即执行内联脚本来设置全局监听器。应用程序已准备好接收事件,但浏览器尚未执行任何应用程序代码。这几乎是零 JavaScript 实现。
HTML 中包含WHERE编码后的元素属性。当用户触发事件时,框架可以使用 DOM 中的信息来延迟创建事件处理程序。创建过程涉及延迟反序列化APP_STATE,FRAMEWORK_STATE以完成操作WHAT。框架延迟创建事件处理程序后,事件处理程序即可处理事件。请注意,客户端不会重复服务器已完成的工作。
关于内存使用情况的说明
DOM 元素在其生命周期内会保留事件处理程序。而水合(hydration)会立即创建所有监听器。因此,水合需要在启动时分配内存。
可恢复框架在事件触发后才会创建事件处理程序。因此,可恢复框架比水合机制消耗的内存更少。此外,可恢复方法在执行完毕后不会保留事件处理程序。事件处理程序执行完毕后会被释放,从而释放内存。
从某种意义上说,释放内存与水合过程相反。这就像框架延迟地为某个事件加载内存WHAT,执行它,然后再将其释放。事件处理程序的第一次执行和第n次执行之间并没有太大区别。事件处理程序的延迟创建和释放并不符合水合的思维模型。
结论
水合操作会造成重复工作,因此会产生额外的开销。服务器会构建 ` WHEREand` 和WHAT`( APP_STATEand FRAMEWORK_STATE)`,但这些信息会被丢弃,而不是序列化后提供给客户端。客户端随后会收到信息不足以重建应用程序的 HTML。信息缺失迫使客户端急于下载应用程序并执行它,以恢复 `and` 和 ` WHERE( WHATand APP_STATE) FRAMEWORK_STATE`。
另一种方法是可恢复性。可恢复性的核心在于将所有信息从服务器传输到客户端。这些信息包含WHERE(和)。这些附加信息使客户端能够在不立即下载应用程序代码的情况下推断应用程序的WHAT运行状态。只有当用户交互发生时,客户端才会下载处理该特定交互的代码。客户端不会重复服务器端的任何工作;因此,不会产生任何额外开销。 APP_STATEFRAMEWORK_STATE
为了将这一理念付诸实践,我们构建了Qwik,一个围绕可恢复性而设计的框架,并实现了卓越的启动性能。我们也期待听到您的反馈!让我们继续交流,共同进步,为用户构建更快速的 Web 应用程序。
PS:非常感谢 Ryan Carniato、 Rich Harris、 Alex Patterson、 Dylan Piercey、 Alex Russell和Steve Sewell 为本文提供的建设性意见。❤️
我们知道您有很多疑问。因此,我们整理了一份常见问题解答,希望能解答您的所有疑问。
为什么要创造一个新术语?
恢复功能并没有明确的界限表明组件 X 是否水合。如果您坚持认为 Qwik 会水合,那么您有两种选择:
- Qwik 应用会在全局事件处理程序注册后进行初始化。但这感觉不太对劲,因为没有下载任何应用代码,也没有执行任何操作。
- Qwik 应用会在首次交互解析序列化状态时进行“水合”(hydration)。水合是指为交互添加事件监听器。反序列化状态用于恢复应用状态,与注册事件处理程序无关。有些情况下不需要进行反序列化,即使需要,也会在事件触发后进行。另一个问题是,反序列化状态甚至会恢复那些尚未下载或永远不会下载的组件的状态。因此,虽然很容易认为这就是水合的目的,但我们将其视为应用状态的惰性反序列化,因为它与事件处理没有直接关系。
这两个方案都不太令人满意,所以我们决定创造一个新术语。你可以把“保持应用的可交互性”定义为“使应用具备交互性”,但这样的定义过于宽泛,适用于所有人,反而降低了它的价值。因此,虽然这听起来像是吹毛求疵,但我们更倾向于用“保持应用的可交互性”来讨论,因为我们认为前者更能体现使应用具备交互性所需工作量的巨大差异。
恢复能力仅仅是指赛后补充水分吗?
这当然是一种合理的视角。然而,两者之间存在一个很大的区别。可恢复性并不要求框架下载并执行组件来了解组件层次结构。可恢复性要求框架的所有信息都序列化到 HTML 中,包括:
- 事件监听器和事件类型的位置。
- 在哪里可以下载活动报告
- 组件边界
- 组件属性
- 投影/儿童
- 如果需要,组件重新渲染函数应该从哪里下载?
由于该框架会反序列化所有这些信息,并从服务器停止的地方继续执行,因此“可恢复”是一个更恰当的词。
实际效果如何?** **哪里可以看到使用可恢复策略的网站?
Builder.io 使用可恢复策略(以及 Qwik)重构了我们的网站。我们移除了 99% 的启动 JavaScript,最终的应用即使在移动设备上也运行得非常流畅。借助 Qwik 和 Partytown,我们成功减少了网站 99% 的 JavaScript,并获得了 100/100 的 PageSpeed 评分。(您仍然可以访问使用水合技术的旧页面[ PageSpeed 50/100 ],并将其与使用可恢复技术的新页面[ PageSpeed 100/100 ] 进行比较,亲身体验性能差异。)
Qwik 的文档运行在 Qwik 平台上。您可以通过在浏览器中打开开发者工具(隐身模式)来查看其底层代码,并注意到启动时没有启用 Javascript。(该页面还使用Partytown将第三方分析迁移到 Web Worker。)
最后,来看看在 Cloudflare Edge 上运行的待办事项应用演示。此页面大约 50 毫秒即可交互!
我的框架能够实现渐进式和/或惰性水化。这两者是一样的吗?
不,因为渐进式/惰性注水仍然无法从服务器停止的地方继续。所有组件代码都需要下载并执行才能恢复和安装事件处理程序。
有了可恢复功能,许多组件由于从未更改,因此永远不会重新加载。但这些组件可以向子组件传递 props,或者创建子组件投影的内容。因此,即使组件不具备交互性,其状态也需要通过重新执行来恢复。正因为需要恢复 props,所以支持渐进式/延迟加载的组件岛不能无限小。
简而言之,恢复功能需要下载和执行的代码量要比水合作用少得多,才能处理用户交互。
我的框架知道如何创建独立区域。这和创建独立区域是一回事吗?
岛屿架构将应用程序拆分成多个独立岛屿。每个岛屿都可以独立进行水合。这样一来,总工作量不再是一次性的大水合,而是分散到多个较小的水合事件中。通常,某个触发器会使岛屿延迟水合,而不是在启动时立即水合。
这意味着基于岛屿的水合作用是一种改进,因为它可以将工作分解成更小的部分并延迟其执行,但这仍然是水合作用,与可恢复性不同。
我的框架知道如何序列化状态。这样做会有开销吗?
问题在于“state”这个词的含义被过度使用了。没错,有些元框架可以序列化状态。但在这里,“state”指的是APP_STATE,而不是FRAMEWORK_STATE。据我所知,没有任何流行的框架(或元框架)可以序列化FRAMEWORK_STATE。此外,即使FRAMEWORK_STATE被序列化了,WHAT和WHERE这两个字段仍然没有被序列化。
是的,状态序列化(APP_STATE)很有用,可以避免客户端的大量工作。但它仍然会导致数据水合。
组分在首次相互作用时是否发生水合作用?
如果你查看可恢复框架的内部状态,你会发现组件的首次交互与后续交互之间并无区别。唯一的区别在于框架已经解析了序列化的状态。一旦状态被解析,它就会应用于所有组件,而不仅仅是用户交互的那个组件。框架随时可以将状态序列化回 HTML。这是否意味着应用程序不再处于“水合”状态?从这个角度来看,状态的反序列化会使所有组件“水合”,即使它们的源代码尚未下载。
我今天如何利用可恢复功能?
框架控制着应用程序用于恢复交互的策略类型。因此,要利用可恢复性,您的应用程序需要使用支持该功能的框架之一。目前,我们只知道 Qwik 明确致力于实现可恢复性。可恢复性的优势显而易见,不容忽视,所以我相信未来会有更多框架开始采用这种策略,无论是新框架还是选择迁移到可恢复性的现有框架。
首次交互是否存在延迟?
如果您使用预取,情况就不同了。Qwik 对预取持开放态度——我们尝试过多种预取策略(主动预取、可见预取、分析驱动预取),并取得了显著成效。在大多数情况下,我们发现使用 Web Worker(例如 Partytown)进行预取能够实现零成本和高速的最佳平衡。我们将把最佳实践融入框架,并随着时间的推移提供包含推荐模式的示例。
我可以在 Qwik 中使用我的 React/Angular/Vue/Svelte 组件吗?
重写应用程序是一项浩大的工程。为了降低入门门槛,我们正在研究如何与目前一些流行的框架实现互操作性。Qwik 将成为您的应用程序编排器,实现应用程序的即时启动,同时还能与您现有的大部分代码兼容。您可以将 Qwik 视为您当前应用程序的编排器。这样,您无需重写整个应用程序,即可获得诸多益处。这项工作仍在进行中,敬请期待。
如果您想知道Qwik接下来会推出什么产品:
如果你读到这里,非常感谢!点击此处了解更多关于 Qwik 的信息,或在 Twitter 上关注我们,以便及时获取最新动态。
文章来源:https://dev.to/builderio/Hydration-is-pure-overhead-33g7