因关闭而死亡(以及 Qwik 如何解决这个问题)
在我们之前的文章中,我们向大家介绍了 Qwik。在那篇文章中,我们略微介绍了许多细节,并承诺稍后会深入探讨。在深入探讨 Qwik 及其背后的设计决策之前,我们有必要先了解一下我们(这个行业)是如何走到今天的。当前这一代框架的哪些假设阻碍了它们获得良好的交互时间得分?通过了解当前这一代框架的局限性,我们可以更好地理解为什么 Qwik 的设计决策乍一看可能令人意外。
让我们谈谈TTI
TTI(可交互时间)衡量的是从导航到 URL 到页面可交互的时间。为了打造响应式网站的外观,SSR(服务器端渲染)必不可少。其思路是:快速向用户展示网站,等他们弄清楚点击什么时,应用程序就会自行引导并安装所有监听器。因此,TTI 实际上是衡量框架安装 DOM 监听器所需时间的指标。
在上图中,我们关注的是启动到可交互的时间。让我们从可交互阶段开始,回溯到框架实现可交互所需的所有步骤。
- 框架需要找到监听器的位置。但框架很难获取这些信息。监听器位于
described
模板中。 - 实际上,我认为“嵌入
embedded
”这个词比“嵌入”更好,described.
因为框架无法轻易获取信息。框架需要执行模板才能获取监听器闭包。 - 要执行模板,需要下载模板本身。但下载的模板包含导入代码,需要下载更多代码。模板需要下载其子模板。
- 我们有了模板,但还没有找到监听器。模板执行的真正含义是将模板与状态合并。没有状态,框架就无法运行模板,也就无法找到监听器。
- 状态需要在客户端下载和/或计算。计算通常意味着需要下载更多代码才能计算状态。
一旦下载了所有代码,框架就可以计算状态,将状态输入模板,最后获取监听器的闭包并将这些闭包安装在 DOM 上。
要达到可交互的状态需要做很多工作。目前所有框架都是这样运作的。最终,这意味着需要下载并执行应用程序的大部分内容,框架才能找到并安装监听器。
让我们谈谈闭包
上面描述的核心问题是下载代码需要大量带宽,框架也需要大量的 CPU 时间来查找监听器,以便页面可以交互。但我们忘记了闭包会封闭代码和数据。这是一个非常方便的特性,也是我们喜欢闭包的原因。但是,这也意味着所有闭包数据和代码都需要在闭包创建时可用,而不是在闭包执行时延迟创建。
让我们看一个简单的 JSX 模板(但其他模板系统也有同样的问题):
import {addToCart} from './cart';
function MyBuyButton(props) {
const [cost] = useState(...);
return (
Price: {cost}
<button onclick={() => addToCart()}>
Add to cart
</button>
);
}
为了实现交互,我们只需要知道监听器的位置。在上面的例子中,这些信息与模板纠缠在一起,如果不下载并执行页面上的所有模板,就很难提取这些信息。
一个页面可能有数百个事件监听器,但绝大多数都不会执行。为什么我们要花时间下载代码并为可能发生的事情创建闭包,而不是等到真正需要的时候再执行呢?
因关闭而死亡
闭包很便宜,而且随处可见。但它们真的便宜吗?是也不是。没错,它们便宜是因为在运行时创建它们很便宜。但是,它们很昂贵,因为它们会封闭代码,而这些代码需要比其他方式更快地下载。它们也很昂贵,因为它们会阻止 tree shake 的发生。因此,我们遇到了一种我称之为“闭包致死”的情况。闭包是放置在 DOM 上的监听器,它们会封闭那些很可能永远不会运行的代码。
页面上的“购买”按钮很复杂,很少被点击。然而,它却迫使我们下载所有相关的代码,因为这正是闭包的作用。
Qwik 使监听器 HTML 可序列化
上文中,我试图强调闭包可能存在隐性成本。这些成本以代码急切下载的形式出现。这使得闭包难以创建,从而阻碍了用户与网站的交互。
Qwik 希望尽可能延迟监听器的创建。为了实现这一点,Qwik 提供了以下租户:
- 监听器需要是 HTML 可序列化的。
- 监听器不会关闭代码,直到用户与监听器交互之后。
让我们看看在实践中如何实现这一点:
<button on:click=”MyComponent_click”>Click me!</button>
然后在文件中:MyComponent_click.ts
export default function () {
alert('Clicked');
}
看一下上面的代码。SSR 在渲染过程中发现了监听器的位置。SSR 不会丢弃这些信息,而是将监听器以属性的形式序列化到 HTML 中。现在,客户端无需重放模板的执行来发现监听器的位置。相反,Qwik 采用了以下方法:
- 安装
qwikloader.js
到页面上。它小于 1KB,执行时间不到 1 毫秒。由于它非常小,最佳做法是将其内联到 HTML 中,从而节省服务器往返时间。 - 可以
qwikloader.js
注册一个全局事件处理程序,并利用冒泡机制一次性监听所有事件。调用次数减少addEventListener
=> 交互速度更快。
结果是:
- 无需下载模板来定位监听器。监听器以属性的形式序列化到 HTML 中。
- 无需执行模板即可检索监听器。
- 无需下载任何状态即可执行模板。
- 现在所有代码都是惰性的,只有当用户与监听器交互时才会下载。
Qwik 简化了当前框架的引导过程,并将其替换为单个全局事件监听器。其最大的优点在于,它与应用程序的大小无关。无论应用程序变得多么庞大,它始终只是一个监听器。由于所有信息都已序列化到 HTML 中,因此需要下载的引导代码是恒定的,并且大小与应用程序的复杂度无关。
总而言之,Qwik 背后的基本理念是可恢复。它从服务器中断的地方继续执行,客户端只需执行 1KB 的数据。而且,无论您的应用程序变得多么庞大和复杂,这段代码都将保持不变。在接下来的几周里,我们将探讨 Qwik 如何独立恢复、管理状态和渲染组件,敬请期待!
我们对 Qwik 的未来以及它所开辟的用途感到非常兴奋。
- 在StackBlitz上尝试
- 在github.com/builderio/qwik上为我们加星
- 在@QwikDev和@builderio上关注我们
- 在Discord上与我们聊天
- 加入builder.io