因关闭而死亡(以及 Qwik 如何解决这个问题)

2025-06-08

因关闭而死亡(以及 Qwik 如何解决这个问题)

在我们之前的文章中,我们向大家介绍了 Qwik。在那篇文章中,我们略微介绍了许多细节,并承诺稍后会深入探讨。在深入探讨 Qwik 及其背后的设计决策之前,我们有必要先了解一下我们(这个行业)是如何走到今天的。当前这一代框架的哪些假设阻碍了它们获得良好的交互时间得分?通过了解当前这一代框架的局限性,我们可以更好地理解为什么 Qwik 的设计决策乍一看可能令人意外。

让我们谈谈TTI

TTI(可交互时间)衡量的是从导航到 URL 到页面可交互的时间。为了打造响应式网站的外观,SSR(服务器端渲染)必不可少。其思路是:快速向用户展示网站,等他们弄清楚点击什么时,应用程序就会自行引导并安装所有监听器。因此,TTI 实际上是衡量框架安装 DOM 监听器所需时间的指标。

时间都去哪儿了

在上图中,我们关注的是启动到可交互的时间。让我们从可交互阶段开始,回溯到框架实现可交互所需的所有步骤。

  1. 框架需要找到监听器的位置。但框架很难获取这些信息。监听器位于described模板中。
  2. 实际上,我认为“嵌入embedded”这个词比“嵌入”更好,described.因为框架无法轻易获取信息。框架需要执行模板才能获取监听器闭包。
  3. 要执行模板,需要下载模板本身。但下载的模板包含导入代码,需要下载更多代码。模板需要下载其子模板。
  4. 我们有了模板,但还没有找到监听器。模板执行的真正含义是将模板与状态合并。没有状态,框架就无法运行模板,也就无法找到监听器。
  5. 状态需要在客户端下载和/或计算。计算通常意味着需要下载更多代码才能计算状态。

一旦下载了所有代码,框架就可以计算状态,将状态输入模板,最后获取监听器的闭包并将这些闭包安装在 DOM 上。

要达到可交互的状态需要做很多工作。目前所有框架都是这样运作的。最终,这意味着需要下载并执行应用程序的大部分内容,框架才能找到并安装监听器。

让我们谈谈闭包

上面描述的核心问题是下载代码需要大量带宽,框架也需要大量的 CPU 时间来查找监听器,以便页面可以交互。但我们忘记了闭包会封闭代码和数据。这是一个非常方便的特性,也是我们喜欢闭包的原因。但是,这也意味着所有闭包数据和代码都需要在闭包创建时可用,而不是在闭包执行时延迟创建。

让我们看一个简单的 JSX 模板(但其他模板系统也有同样的问题):

import {addToCart} from './cart';

function MyBuyButton(props) {
  const [cost] = useState(...);
  return (
    Price: {cost}
    <button onclick={() => addToCart()}>
      Add to cart
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

为了实现交互,我们只需要知道监听器的位置。在上面的例子中,这些信息与模板纠缠在一起,如果不下载并执行页面上的所有模板,就很难提取这些信息。

一个页面可能有数百个事件监听器,但绝大多数都不会执行。为什么我们要花时间下载代码并为可能发生的事情创建闭包,而不是等到真正需要的时候再执行呢?

因关闭而死亡

闭包很便宜,而且随处可见。但它们真的便宜吗?是也不是。没错,它们便宜是因为在运行时创建它们很便宜。但是,它们很昂贵,因为它们会封闭代码,而这些代码需要比其他方式更快地下载。它们也很昂贵,因为它们会阻止 tree shake 的发生。因此,我们遇到了一种我称之为“闭包致死”的情况。闭包是放置在 DOM 上的监听器,它们会封闭那些很可能永远不会运行的代码。

页面上的“购买”按钮很复杂,很少被点击。然而,它却迫使我们下载所有相关的代码,因为这正是闭包的作用。

Qwik 使监听器 HTML 可序列化

上文中,我试图强调闭包可能存在隐性成本。这些成本以代码急切下载的形式出现。这使得闭包难以创建,从而阻碍了用户与网站的交互。

Qwik 希望尽可能延迟监听器的创建。为了实现这一点,Qwik 提供了以下租户:

  1. 监听器需要是 HTML 可序列化的。
  2. 监听器不会关闭代码,直到用户与监听器交互之后。

让我们看看在实践中如何实现这一点:

<button on:click=MyComponent_click>Click me!</button>
Enter fullscreen mode Exit fullscreen mode

然后在文件中:MyComponent_click.ts

export default function () {
  alert('Clicked');
}
Enter fullscreen mode Exit fullscreen mode

看一下上面的代码。SSR 在渲染过程中发现了监听器的位置。SSR 不会丢弃这些信息,而是将监听器以属性的形式序列化到 HTML 中。现在,客户端无需重放模板的执行来发现监听器的位置。相反,Qwik 采用了以下方法:

  1. 安装qwikloader.js到页面上。它小于 1KB,执行时间不到 1 毫秒。由于它非常小,最佳做法是将其内联到 HTML 中,从而节省服务器往返时间。
  2. 可以qwikloader.js注册一个全局事件处理程序,并利用冒泡机制一次性监听所有事件。调用次数减少addEventListener=> 交互速度更快。

结果是:

  1. 无需下载模板来定位监听器。监听器以属性的形式序列化到 HTML 中。
  2. 无需执行模板即可检索监听器。
  3. 无需下载任何状态即可执行模板。
  4. 现在所有代码都是惰性的,只有当用户与监听器交互时才会下载。

Qwik 简化了当前框架的引导过程,并将其替换为单个全局事件监听器。其最大的优点在于,它与应用程序的大小无关。无论应用程序变得多么庞大,它始终只是一个监听器。由于所有信息都已序列化到 HTML 中,因此需要下载的引导代码是恒定的,并且大小与应用程序的复杂度无关。

总而言之,Qwik 背后的基本理念是可恢复。它从服务器中断的地方继续执行,客户端只需执行 1KB 的数据。而且,无论您的应用程序变得多么庞大和复杂,这段代码都将保持不变。在接下来的几周里,我们将探讨 Qwik 如何独立恢复、管理状态和渲染组件,敬请期待!

我们对 Qwik 的未来以及它所开辟的用途感到非常兴奋。

鏂囩珷鏉ユ簮锛�https://dev.to/builderio/death-by-closure-and-how-qwik-solves-it-44jj
PREV
10 个迹象表明你写代码太多了
NEXT
初看 Bun:它真的比 Node.js 和 Deno 快 3 倍吗?