预取

Qwik 提供了各种策略来提前预取模块,并且在此基础上,Qwik City 还能通过使用推测性模块获取进一步提升。本文档页面描述了 Qwik 预取的低级特性。然而,建议参考 Qwik City 的推测性模块获取文档以获得更全面的理解。

预取是应用程序在用户实际需要这些模块之前,通过后台任务开始下载模块的一种方式。理想的解决方案是仅预取用户交互中高度可能执行的最小代码量,同时避免任何不会被使用的 JavaScript。

仅下载和执行最小量的 JavaScript 是 Qwik 应用程序擅长的领域。由于 Qwik 能够理解如何使用各个组件(以及什么不使用),它还可以最好地决定应该预取哪些捆绑包。

请记住,可恢复性和水合之间的区别在于,可恢复性允许 Qwik 应用程序避免执行 JavaScript 来恢复事件监听器、组件树和应用程序状态。通过基本上分解组件的事件监听器、渲染函数和状态,与传统方法相比,预取的代码量也显著减少。

收集已使用的符号

当 Qwik 渲染应用程序时,它能够收集渲染过程中使用的“符号”。符号包括组件的各个部分,这些部分由优化器提取出来以拆分应用程序。单独的事件监听器、组件状态和组件渲染器本身都是可能提取的不同符号的示例。

例如,考虑一个产品页面,除了一个“添加到购物车”按钮外,大部分内容都是静态的。当点击此按钮时,用户应立即获得反馈,以显示产品已添加到购物车。在此示例中,Qwik 优化器能够理解用户唯一可能与之交互的符号是“添加到购物车”按钮的点击事件监听器。

对于我们的“添加到购物车”示例,优化器将仅收集点击事件监听器的符号以及添加到购物车小部件的渲染器。然而,它不必下载、水合和重新渲染应用程序的任何其他部分,因为重新渲染页面的其他部分根本不可能。

由于 Qwik 理解可能性,它能够仅预取事件监听器的代码,而不是整个应用程序或路由的所有 JavaScript。这与传统方法相反,在传统方法中,必须预取整个应用程序或路由以及框架代码才能添加点击事件监听器。

预取策略

预取策略是决定 Qwik 是否应该在后台预取哪些 JavaScript 的逻辑。默认情况下,Qwik 将预取页面上任何可见的监听器。要配置预取策略,请使用 renderToStream() 函数的选项参数,通常可以在 src/entry.ssr.tsx 源文件中找到。提供最佳的预取策略是 Qwik 将继续研究和实验的领域。

对于 Qwik City 应用程序,我们强烈推荐使用推测性模块获取

export default function (opts: RenderToStreamOptions) {
  return renderToStream(<Root />, {
    manifest,
    prefetchStrategy: {
      // 自定义预取配置
    },
    ...opts,
  });
}

预取实现

浏览器提供了许多方法来“实现”或应用预取策略,并且 Qwik 可以配置为优先选择一种实现,每种实现都有其优缺点。根据配置,生成的 HTML 内容将包括预取实现。

export default function (opts: RenderToStreamOptions) {
  return renderToStream(<Root />, {
    manifest,
    prefetchStrategy: {
      implementation: {
        // 自定义预取实现
      },
    },
    ...opts,
  });
}

对于 Qwik City 应用程序,我们强烈推荐使用推测性模块获取,它使用 prefetchEvent 实现。

选项描述
prefetchEvent使用包含应预取的 URL 的 detail 数据分发 qprefetch 事件。事件分发脚本将内联到文档的 HTML 中。默认情况下,prefetchEvent 实现将设置为 always
linkInsert<link> 元素插入文档中。当使用 html-append 时,它将直接在 html 中渲染每个 <link>,并将其附加到 body 的末尾。使用 js-append 选项时,它将插入一些 JavaScript,该 JavaScript 在运行时创建元素并将其附加到 body 的末尾。
linkRel此选项用于定义 <link> 元素的 rel 属性。当使用 linkInsert 选项时,默认值为 prefetch。其他选项包括 preloadmodulepreload
workerFetchInsert通过为每个模块调用 fetch() 来预取 URL,以填充网络缓存。

分发的预取事件

推测性模块获取是 Qwik City 使用的首选缓存策略。该策略监听由 Qwik 框架分发的 qprefetch 事件。该事件包含一个 URL 列表,后台线程应使用这些 URL 来预填充浏览器的缓存

Qwik 本身应配置为使用 prefetchEvent 实现,它将分发一个 qprefetch 事件。默认情况下,prefetchEvent 实现将设置为 always。接下来,Qwik City将监听此事件,并与其服务工作线程通信,以将请求/响应对象对持久化,以便将其缓存在长期内存中。

通过使用服务工作线程拦截浏览器的 fetch 请求,这种方法允许对缓存进行细粒度控制,并防止对同一资源的重复请求。

以下是手动分发事件的示例。这些事件是由 Qwik 本身分发的,不需要开发人员手动分发这些事件。此外,服务工作线程将自动为这些事件添加侦听器。

dispatchEvent(new CustomEvent("qprefetch", { detail: {
  bundles: [...]
}}));

使用带有 rel 属性的 <link> 元素是当今框架的常见方法,Qwik 可以通过配置 linkInsertlinkRel 选项来使用此方法。使用 link rel 方法的挑战在于在撰写本文时,_所有_设备上都不支持它。此外,在开发过程中,它可能会误导人们认为它在任何地方都有效;而在移动设备上,很难看到链接预取是否正常工作。

例如,Safari(驱动 iPhone 和 iPad 的浏览器)不支持 modulepreload。这很重要,因为移动设备可能最能从模块预加载中受益。接下来是 Firefox,在 https 上不支持链接 rel prefetch

预取是一项旨在帮助加快访问者体验的功能,但是如果浏览器和 CDN/服务器的组合不正确,它实际上可能会使体验变慢!

- Rel=prefetch and the Importance of Effective HTTP/2 Prioritisation

此外,可能会对同一资源发起多个请求。例如,假设我们要预取 module-a.js,而在下载该文件时(可能需要很短的时间或很长的时间),用户与应用程序进行交互,然后决定实际请求和执行 module-a.js。撰写本文时,浏览器通常会发起第二个请求,这使情况变得更糟。

Web Worker Fetch

workerFetchInsert 指示 Qwik 使用 Web Worker 来 fetch() JavaScript 文件,目的是使用模块预热浏览器缓存。通过使用 Web Worker,获取和缓存逻辑位于另一个线程上。获取响应还将具有 immutable 或长时间的缓存控制头,以便浏览器不会发起第二个网络请求。

这种设置的缺点是获取的响应被丢弃,只有在浏览器级别上才有希望缓存该文件。

常见的预取问题

问题用户事件上的延迟加载是否会很慢,因为用户必须等待代码下载?

是的,这会导致明显的延迟,尤其是在慢速的 3G 网络上。这就是为什么代码预取是 Qwik 应用程序的重要部分。

代码预取确保在导航到页面时立即获取运行应用程序所需的所有代码。这样,当用户执行操作时,该操作的代码来自预取缓存而不是网络。结果是代码执行是即时的。

问题代码预取不会导致与现有框架下载并急切执行所有代码的行为相同吗?

不会,有几个原因:

  • 现有框架必须在应用程序可以交互之前下载并执行所有代码(水合)。通常,代码的下载是较小的时间成本,而代码的执行是较大的时间成本。
  • Qwik 代码预取仅下载而不执行代码。因此,即使 Qwik 预取与现有框架预取相同数量的代码,结果也会显著节省时间成本。
  • Qwik 仅预取当前页面所需的代码。Qwik 避免下载与静态组件相关的代码。在最坏的情况下,Qwik 预取与现有框架的最佳情况相同数量的代码。在大多数情况下,与现有框架相比,Qwik 预取的代码量要小得多。
  • 代码的预取可以在主线程之外的其他线程上进行。许多浏览器甚至可以在主线程之外预解析代码的 AST。
  • 如果用户交互发生在预取完成之前,浏览器将自动优先处理交互块,然后再处理剩余的预取块。
  • Qwik 可以将应用程序分解为许多小块,并且这些块可以按照用户与之交互的概率顺序下载。现有框架难以将应用程序分解为小块,并且没有简单的方法来优先下载块的顺序,因为水合需要一个单一的“主”入口点到应用程序。

问题谁负责知道要预取哪些代码?

Qwik 可以作为 SSR 渲染的一部分自动生成预取指令。通过执行应用程序,Qwik 在运行时了解哪些组件可见,用户可以触发哪些事件以及哪些代码需要下载。结果是预取是适用于此页面的理想文件集。除了将预取策略添加到 renderToStream() 中之外,开发人员不需要采取任何其他操作。

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • adamdbradley
  • RATIU5
  • manucorporat
  • literalpie
  • saikatdas0790
  • the-r3aper7
  • mhevery