可恢复性,WTF?
也许你最近听说过“可恢复性”这个术语。也许有人对 Miško Hevery 的新Qwik框架赞不绝口。也许你听过我在即将发布的Marko 6工作中提到过它。也许你听说过它与“水合”(Hydration)有关。但你甚至不清楚“水合”是什么。
这篇文章正是为你而写的。
JavaScript 开发人员为何如此渴望?
关于“水合”(这也是问题的一部分)有很多定义。但我最喜欢的是:
Hydration 是将服务器渲染的应用恢复到客户端渲染状态的过程。(来源:@mlrawlings)
为什么会有这种事?它并不总是这样。在过去,人们会在我们选择的后端服务器渲染页面,然后添加一些 JavaScript 来处理交互。也许是一些 jQuery。
随着对交互性的需求不断增长,我们在代码中增加了更多结构。命令式框架逐渐演变成了如今我们熟知并喜爱的声明式框架,例如 React、Angular 和 Vue。现在,UI 的整个呈现都基于 JavaScript。
为了重新获得服务器渲染的能力,这些框架也在服务器上运行以生成 HTML。这样我们就可以用一种语言编写和维护一个应用程序。当我们的应用程序在浏览器中启动时,这些框架会重新运行相同的代码,添加事件处理程序并确保应用程序处于正确的状态。正是这种“re-hydration”(后简称为 hydration)使应用程序具有交互性。
听起来还不错吧?不过,还有一个问题。
进入恐怖谷
随着页面规模增大,在页面加载时重新渲染整个应用程序可能会非常耗时,尤其是在网络和设备速度较慢的情况下。这实际上并不会重新创建 DOM 节点,但该过程会像重新创建 DOM 节点一样遍历所有应用程序代码。
这样做有两个问题。首先,你可以看到服务器渲染的页面,但在 JavaScript 加载、解析和执行之前,它无法交互。
您可以让页面支持无 JavaScript 的回退机制,但在 JavaScript 执行之前,它无法提供相同的用户体验。有人可能点击了按钮,但没有任何提示,什么也没发生。或者有人触发了整个页面的重新加载,然后又重新开始。如果他们多等半秒钟,客户端交互就会变得流畅。
其次,执行成本可能很高。它可能会阻塞主线程。用户尝试操作页面(例如滚动或输入文本字段)时可能会遇到输入延迟。
这不是最好的体验。
那么可恢复性是什么?
顾名思义:执行一些工作,暂停,然后恢复。这个过程允许框架避免在浏览器启动应用程序时进行额外的工作,而是利用应用程序在服务器上执行时发生的情况。有点像一台休眠的计算机,唤醒后会回到你之前离开的位置。只不过,可恢复性跨越了服务器/浏览器网络边界。
从某种意义上来说,它实现起来比较复杂,但从另一个角度来看却非常简单。它在启动时附加一些全局事件处理程序,然后只在交互时运行必要的代码。
听起来很熟悉?这不就是我们以前用 Vanilla JavaScript 或 jQuery 做的事情吗?
最大的区别在于,您可以用现代方式编写代码。它是一个跨服务器和浏览器运行的单一应用。它具有声明式和可组合性。所有您喜欢的框架都具备的优势,一应俱全。
那么……水合机制到底是什么?为什么不是所有东西都可以恢复?
这很难做到。现代声明式框架是数据驱动的。当发生事件时,你会更新某些状态,然后某些组件就会重新渲染。这就是挑战所在。
如果组件从未执行创建事件处理程序的操作,那么状态又如何存在于事件处理程序中呢?我们现代的框架充斥着一堆函数,它们不断覆盖值。
function Counter() {
const [count, setCount] = useState(0);
// How can I call `increment` without ever running
// Counter once? Where does `count` and `setCount`
// come from?
const increment = () => setCount(count + 1);
return <button
className="counter-button"
onClick={increment}
>
{count}
</button>;
}
我们需要独立于组件进行更新,并且需要全局可用。我们需要响应式状态,并且需要撤消代码中所有使用闭包的操作。
// Over-simplified example:
// global scope
const lookup = [
...,
Counter,
increment,
...,
{
count: createReactiveStateWhenAccessedFirstTime({
value: 0,
watchers: [Counter]
})
}
]
// global event handler
function increment(event, ctx) {
ctx.count++; // update value and trigger watchers
// ie.. only run the component for the first time now.
}
// global event listener
document.addEventListener("click", (event) => {
// find the element we care about
if (event.target.className === "counter-button") {
// find the location of its handler and data from it
const fn = lookup[event.target.$$handlerID];
const context = lookup[event.target.$$handlerContext];
fn(event, context);
}
});
更重要的是,我们需要从服务器传递应用程序的完整状态。不仅仅是你在应用中管理的状态数据,还包括框架的内部状态。上面的示例过于简单,但这个全局作用域需要包含我们应用的所有数据。
实现这一点并不容易,而且也需要权衡。
序列化
除了编译负担更重之外,可恢复性还依赖于它能够序列化的内容。而这方面的要求可能更高。典型的服务器渲染应用程序会将应用程序的初始状态存储在两个地方:硬编码到您编写的 JavaScript 源代码中,以及以序列化 JSON 的形式写入页面。后者是我们获取服务器执行时生成的所有动态和异步数据的方式。
// in the code
const [count, setState] = useState(0);
// in JSON
<script id="__NEXT_DATA__" type="application/json">
...
</script>
我们需要这样做,因为当我们在浏览器中唤醒应用程序时,我们的 JavaScript 代码需要与当前呈现的 HTML 处于相同的状态。
您可能会想,“我们不能直接从 HTML 中提取这些信息吗?”
可以,但也不可以。最终输出仅包含最终格式化的数据。这可能会造成损失。
const [date, setDate] = useState(Date.now());
const [dateFormat, setDateFormat] = useState("MM/DD/YYYY")
return <time>{format(date, dataFormat)}</time>
// in HTML - how do I get the timestamp?
<time>08/19/2022</time>
想象一下,一个格式化的日期不包含时间,但用户界面允许用户将格式更改为包含时间的格式。仅使用 HTML 不足以获取这些信息。
可恢复性确实有类似的要求,即在服务器渲染时获取框架内部状态,而不是仅仅依赖于我们通常序列化的应用程序数据。至少,我们需要序列化传入每个组件的所有 props,以便它们能够被独立唤醒,而无需预先运行整个组件树。
三个火枪手
幸运的是,要使代码可恢复,需要大量关于浏览器中哪些内容可以更新的知识,因为你需要知道哪些事件可以更新哪些 UI。因此,可恢复性通常与其他两种优化措施相结合。
有一种优化被称为渐进式水合(有时也称为选择性水合)。在可恢复框架中,您实际上并没有进行水合,但仍然可以将代码加载推迟到需要的时候。这可以大幅减少打包体积,并实现“默认 0KB JavaScript”。这有助于提高页面加载指标,但它会将工作推迟到您进行交互时。对于可恢复性来说,这仅仅是加载和解析成本,因为它不需要这些 JavaScript 来急切运行。但这仍然需要谨慎操作,建议您预加载任何关键的页面交互。
当应用于多页应用程序 (MPA) 中的服务器渲染页面时,我们还可以知道哪些页面永远不会更新。这使得框架可以跳过发送代码或序列化任何只需在服务器上运行的组件的数据。这在很大程度上抵消了可恢复性的成本。
这被称为部分水合;你可能在Marko、Astro或Fresh等框架中见过这种技术的一个版本,即Islands。不同之处在于,可恢复框架可以在比 Islands 小得多的子组件级别执行此操作,并且可以自动执行。
需要注意的是,虽然这些优化通常以三种方式组合出现,但它们各自独立运作。可恢复性与代码何时加载或加载了多少无关。
生活在一个可恢复的世界
我们还没走到这一步,这还需要一些时间。但我们正在弥补单页应用服务器渲染带来的缺陷。我们创造了 Hydration 这个怪物,现在我们必须打败它。最大的挑战是如何彻底缓解目前 MPA 带来的性能权衡。
在我们能够弥合这方面的差距之前,这一切都依赖于路由,因此,哪些权衡是值得的,这个问题仍需探讨。我们需要所有环节协同工作,才能让这种方法有机会证明其可行性。
但如果真的实现了,它最终将统一传统命令式 jQuery 的 Web 框架与现代声明式 JavaScript 框架的机制。甚至可能从我们的 Web 词汇中抹去“可恢复性”和“水合性”等词汇的必要性。
这是值得努力追求的。
文章来源:https://dev.to/this-is-learning/resumability-wtf-2gcm