推测模块获取

Qwik 能够以极快的速度加载页面并使其交互,因为它能够在没有 JavaScript 的情况下启动。此外,推测模块获取是一个强大的功能,它允许 Qwik 在后台线程中预填充浏览器的缓存。

Qwik 的目标是通过仅缓存基于用户潜在交互的应用程序的必要部分来优化加载。它通过了解哪些交互是不可能的来避免加载不必要的捆绑包。

预填充缓存

每次页面加载都会使用可能在该时刻由用户在页面上执行的捆绑包预填充缓存。例如,假设页面上的按钮有一个点击监听器。当页面加载时,服务工作者的第一个任务是确保该点击监听器的代码已在缓存中。当用户点击按钮时,Qwik 向事件监听器的函数及其任何代码依赖项发出请求以执行该函数。目标是让代码已在浏览器缓存中,准备执行。

初始页面加载为下一个可能的交互准备缓存,并在单独的线程中增量下载其他必要的代码。当发生后续交互(例如打开模态或菜单)时,Qwik 会发出另一个事件,其中包含自上次交互以来可能使用的其他代码。预填充缓存会随着用户与应用程序的交互而持续进行。

预填充缓存事件

推荐的策略是使用服务工作者来填充浏览器缓存。Qwik 框架本身应该使用prefetchEvent 实现,这是默认设置。

使用服务工作者预填充缓存

传统上,服务工作者用于缓存应用程序使用的所有或大多数捆绑包。服务工作者通常只被视为使应用程序脱机工作的一种方式。

Qwik City 使用服务工作者以完全不同的方式来提供强大的缓存策略。目标不是下载整个应用程序,而是使用服务工作者动态地预填充可能执行的缓存。通过下载整个应用程序,可以释放资源,使用户能够仅请求完成他们在屏幕上当前任务所需的必要部分。

此外,服务工作者会自动为从 Qwik 发出的这些事件添加监听器。

后台任务

使用服务工作者的一个优势是它也是工作者的扩展,它在后台线程中运行。

Web 工作者使在与 Web 应用程序的主执行线程分离的后台线程中运行脚本操作成为可能。这样做的好处是,可以在单独的线程中执行繁重的处理,从而使主线程(通常是 UI)能够运行而不会被阻塞/减慢。

通过从服务工作者(它是一个工作者)中预填充缓存,我们能够在后台任务中运行代码,从而不干扰主 UI 线程。通过不干扰主线程,我们可以提高 Qwik 应用程序对用户的性能。

交互式预填充缓存

Qwik 本身应该配置为使用prefetchEvent 实现。这是默认设置。当 Qwik 发出事件时,服务工作者注册会主动将事件数据转发到已安装并处于活动状态的服务工作者。

在后台线程中运行,服务工作者然后获取模块并将它们添加到浏览器的缓存中。主线程只需要发出有关所需捆绑包的数据,而服务工作者的唯一重点是缓存这些捆绑包。

  1. 如果浏览器已经缓存了它?太好了,什么也不做!
  2. 如果浏览器还没有缓存此捆绑包,那么让我们启动获取请求。

服务工作者确保对同一捆绑包的多个请求不会同时发生

缓存请求和响应对

在许多传统框架中,首选策略是使用 html <link> 标签,其 rel 属性设置为 prefetchpreloadmodulepreload。但是,由于已知问题,Qwik 避免使用这种方法作为默认的预取策略,如果需要,它仍然可以配置

相反,Qwik 偏向于使用一种更现代的方法,充分利用浏览器的 缓存 API,与 modulepreload 相比,缓存 API 的支持度更高。

缓存 API

缓存 API 通常与服务工作者相关联,它是一种将请求和响应对存储起来的方式,以便应用程序能够离线工作。除了使应用程序能够在没有连接的情况下工作之外,相同的缓存 API 还为 Qwik 提供了一种极其强大的缓存机制。

使用已安装并激活的 服务工作者 来拦截请求,Qwik 能够处理对已知捆绑包的特定请求。与服务工作者的常见使用方法相反,默认情况下不会尝试处理所有请求,只处理由 Qwik 生成的已知捆绑包。网站已安装的服务工作者仍然可以 由每个网站自定义

Qwik 优化器的一个优势是它会生成一个 q-manifest.json 文件。q-manifest.json 包含一个详细的模块图,说明捆绑包是如何关联的以及每个捆绑包中包含哪些符号。相同的模块图数据也会提供给服务工作者,允许通过缓存处理对已知捆绑包的所有网络请求。

动态导入和缓存

当 Qwik 请求一个模块时,它会使用一个动态的 import()。例如,假设用户发生了交互,要求 Qwik 对 /build/q-abc.js 执行动态导入。执行此操作的代码如下所示

const module = await import('/build/q-abc.js');

这里重要的是,Qwik 本身并不了解预取或缓存策略。它只是对一个 URL 发出请求。但是,由于我们安装了服务工作者,并且服务工作者正在拦截请求,因此它能够检查 URL 并说:“看,这是一个对 /build/q-abc.js 的请求!这是我们的捆绑包之一!让我们先检查一下缓存中是否已经存在此捆绑包,然后再进行实际的网络请求。”

这就是服务工作者和缓存 API 的强大之处!在另一个线程中,Qwik 会预先填充用户可能很快请求的模块的缓存。如果这些模块已经缓存,那么浏览器就不需要做任何事情。

并行化网络请求

缓存请求和响应对 文档中,我们解释了 缓存服务工作者 API 的强大组合。但是,在 Qwik 中,我们可以更进一步,确保不会为同一个捆绑包创建重复的请求,并防止网络瀑布,所有这些都可以在后台线程中完成。

避免重复请求

例如,假设最终用户当前的连接速度非常慢。当他们第一次请求登录页面时,设备会下载 HTML 并渲染内容(Qwik 真正擅长的领域)。在这种缓慢的连接下,如果用户不得不下载数百 KB 的数据才能 使他们的应用程序工作并变得交互式,那将是一件很糟糕的事情。

但是,由于应用程序是用 Qwik 构建的,因此最终用户不需要下载整个应用程序就能使其变得交互式。相反,最终用户已经下载了 SSR 渲染的 HTML 应用程序,并且任何交互式部分,例如“添加到购物车”按钮,都可以立即预取。

请注意,我们只预取了实际的监听器代码,而不是组件树渲染函数的整个堆栈。

在这个非常常见的现实世界示例中,设备的连接速度很慢,设备会立即开始预先填充最终用户可见的可能交互的缓存。但是,由于连接速度很慢,即使我们尽快在 后台线程 中开始缓存模块,但 fetch 请求本身可能仍在进行中。

为了演示目的,假设此捆绑包的获取需要两秒钟。但是,在查看页面一秒钟后,用户点击了按钮。

在传统的框架中,很有可能什么都不会发生!如果框架还没有完成下载、水合和重新渲染,那么事件监听器就无法添加到按钮中。这意味着用户的交互将丢失。

使用 Qwik 的缓存策略,如果用户点击了一个按钮,而我们在一秒钟前就启动了请求,并且距离完全接收请求只有一秒钟,那么最终用户只需要等待那一秒钟。请记住,在本演示中,他们的连接速度很慢。幸运的是,用户已经收到了完全渲染的登录页面,并且正在查看一个完整的页面。接下来,他们只是用他们可以交互的应用程序部分预先填充缓存,并且他们的缓慢连接专门用于这些捆绑包。这与他们的缓慢连接下载整个应用程序以执行一个监听器形成对比。

Qwik 可以拦截对已知捆绑包的请求。如果 fetch 请求在后台线程中正在进行,并且用户请求了同一个捆绑包,它将确保第二个请求能够重新使用同一个捆绑包,该捆绑包可能已经完成下载。尝试使用 link 执行任何这些操作也说明了为什么 Qwik 偏向于使用缓存 API 并使用服务工作者拦截请求作为默认设置,而不是使用 link

减少网络瀑布

网络瀑布是指多个请求按顺序一个接一个地发出。这种顺序过程会导致性能显著下降,因为下载所有必要模块的时间会延长,与所有模块同时并行开始下载的情况相比。

下面是一个包含三个模块 A、B 和 C 的示例。模块 A 导入 B,B 导入 C。HTML 文档是通过首先请求模块 A 来启动瀑布的。

import './b.js';
console.log('Module A');
import './c.js';
console.log('Module B');
console.log('Module C');
<script type="module" src="./a.js"></script>

在这个示例中,当第一次请求模块 A 时,浏览器并不知道它也应该开始请求模块 BC。它甚至不知道它需要开始请求模块 B,直到模块 A 完成下载之后。这是一个常见的问题,因为浏览器事先不知道它应该开始请求什么,直到每个模块完成下载之后。

但是,由于我们的服务工作者包含从清单中生成的模块图,因此我们知道所有被请求的模块。因此,当用户交互或对捆绑包进行预取时,浏览器会启动对所有被请求的捆绑包的请求。这使我们能够大幅缩短请求所有捆绑包所需的时间。

用户服务工作者代码

Qwik City 安装的默认服务工作者仍然可以完全由应用程序控制。例如,源文件 src/routes/service-worker.ts 会变成 /service-worker.js,这是浏览器请求的脚本。请注意,它在 src/routes 中的位置仍然遵循相同的基于目录的路由模式。

下面是一个默认的 src/routes/service-worker.ts 源文件示例

import { setupServiceWorker } from '@builder.io/qwik-city/service-worker';
 
setupServiceWorker();
 
addEventListener('install', () => self.skipWaiting());
 
addEventListener('activate', (ev) => ev.waitUntil(self.clients.claim()));

可以修改 src/routes/service-worker.ts 的源代码,包括选择加入或选择退出设置 Qwik City 的服务工作者。

请注意,setupServiceWorker() 函数是从 @builder.io/qwik-city/service-worker 导入的,并在源文件顶部执行。开发人员可以根据自己的需要灵活地修改此函数的调用时间和位置。例如,如果开发人员希望首先处理 fetch 请求,他们可以在 setupServiceWorker() 上方添加自己的 fetch 监听器。或者,如果他们根本不想使用 Qwik City 的服务工作者,他们只需从文件中删除 setupServiceWorker()

此外,默认的 src/routes/service-worker.ts 文件附带了一个 安装激活 事件监听器,每个监听器都添加到文件底部。提供的回调是推荐的回调。但是,开发人员可以根据自己的应用程序要求修改这些回调。

另一个重要的注意事项是,Qwik City 的请求拦截针对 Qwik 捆绑包,它不会尝试处理任何不是其构建的一部分的请求。

因此,虽然 Qwik City 提供了一种帮助预取和缓存捆绑包的方法,但它并没有完全控制应用程序的服务工作者。这仍然允许开发人员添加自己的服务工作者逻辑,而不会与 Qwik 发生冲突。

开发期间禁用

推测性模块获取只会在预览或生产构建中启动。在开发过程中,服务工作者会被禁用,这也会禁用推测性模块获取。这是因为在开发过程中,我们始终希望确保使用最新的开发代码,而不是之前缓存的代码。

HTTP 缓存与服务工作者缓存

推测性模块获取可能看起来不起作用,部分原因是存在多个级别的缓存。例如,浏览器本身可能会在其 HTTP 缓存 中缓存请求,而服务工作者可能会在 缓存 API 中缓存请求。只清空其中一个缓存可能不足以看到推测性模块获取的效果。

误导性的清空缓存和强制重新加载

当开发人员运行 清空缓存和强制重新加载 时,这有点误导,因为它实际上清空了浏览器的 HTTP 缓存。但是,它并没有清空服务工作者的缓存。即使浏览器的 HTTP 缓存为空,服务工作者仍然保留了之前缓存的请求。

此外,当使用“清空缓存和强制重新加载”时,浏览器会在请求中向服务器发送一个 no-cache 缓存控制头。由于请求具有 no-cache 缓存控制头,因此服务工作者会故意不使用自己的缓存,而是浏览器再次执行通常的 HTTP fetch。

清空服务工作者缓存

测试推测性模块获取的推荐方法是

  • 取消注册服务工作者:在 Chrome DevTools 中,转到“应用程序”选项卡,在“服务工作者”下,单击您网站的服务工作者的“取消注册”链接。
  • 删除“QwikBuild”缓存存储:在 Chrome DevTools 中,转到“应用程序”选项卡,在左侧的“缓存存储”下,右键单击“QwikBuild”缓存存储,然后选择“删除”。
  • 不要强制重新加载:不要强制重新加载,这会向服务工作者发送一个 no-cache 缓存控制头,只需单击 URL 栏并按回车键。这将发送一个正常的请求,就像您是第一次访问者一样。

请注意,此过程仅用于测试推测性模块获取,对于新的构建来说不是必需的。每次构建都会创建一个新的服务工作者,旧的服务工作者会自动取消注册。

调试模式

Qwik 核心中的服务工作者使用 <PrefetchServiceWorker /><PrefetchGraph /> 组件在 root.tsx 中,它有一个调试模式。

要查看服务工作者日志,请将 window.qwikPrefetchSW.push(['verbose', '', []]) 添加到 JavaScript 控制台中,然后按 Enter 键。

贡献者

感谢所有帮助改进此文档的贡献者!

  • ulic75
  • mhevery
  • adamdbradley
  • hamatoyogi
  • manucorporat
  • mrhoodz
  • thejackshelton
  • zanettin
  • wtlin1228
  • aendel
  • jemsco