Qwik 1.14.0:Preloader 登场——革新性的提升

267 阅读8分钟

我们非常兴奋地宣布 Qwik 1.14.0 的发布,这是自 1.0.0 发布以来对 Qwik 最根本的更新!

你可以通过 npm create qwik@latest 在新的 Qwik 项目上尝试,或者通过 npm i @builder.io/qwik@latest @builder.io/qwik-city@latest eslint-plugin-qwik@latest 更新你现有的项目。

Qwik 回顾

使 Qwik 如此快速的核心特性之一是我们称之为"Javascript 流式传输"的功能 - 在所有代码下载完成之前,一旦代码准备就绪就能执行部分代码的独特能力 - 这加速了你网站的交互时间(TTI),类似于视频流由于缓冲和按需交付,比下载整个视频然后播放要快得多。

这一机制首先在构建时通过Qwik optimizer启用,它寻找 $ 符号并将应用程序代码分割成更小的片段(称为"segments")。然后打包工具(Rollup)将这些片段分组到小型 JavaScript 文件(称为"bundles")中,这些文件将相关片段保存在一起。

目标是使 bundle 既适中的大小,每个 bundle 对应 UI 的一个交互部分。

然后在运行时,框架可以利用 service-worker 预取和缓存页面上所有可用的 bundle,这样当用户与某个组件交互时,其代码已经被缓冲并可以立即"惰性执行"(与按需"惰性加载"不同)。

如果用户进行交互但网络非常慢,因此预取仍在进行中,Qwik 将优先下载这些 bundle 并在它们可用时立即执行它们。

相比之下,其他框架需要执行至少所有可见 UI 代码来附加事件监听器,因此它们必须在执行之前下载所有代码,这个过程称为"水合"。

对于基于水合的应用程序在慢速 3G 网络上,TTI 通常达到约 20 秒,对于更复杂的应用程序甚至可能超过 60 秒,因为它与需要水合的组件数量成正比增加。

另一方面,使用 Qwik 在慢速 3G 网络上,TTI 通常可以快至约 5 秒(甚至更快),无论应用程序变得多么复杂。

JavaScript 流式传输实现了加载时间性能的无缝扩展,从简单的营销网站到企业级应用程序。

长期存在的"缓冲"问题

在 Qwik 1.11.0 和 1.13.0 中,我们修复了一些长期存在的导致某些条件下"预取不足"和"预取过度"的错误。通过这些修复,我们确保了页面上所有且仅有用户交互所需的 bundle 被预取,从而防止任何类型的网络延迟。

但是我们最近通过在使用过的低端智能手机上进行手动测试才发现了两个隐藏的问题。事实证明,通过 service worker 从缓存中提供 bundle 是有一定成本的:

  • 对于旧/低端设备,CPU 性能较差,Qwik 组件的首次交互会导致小延迟,即使在良好的网络连接下也是如此。
  • 注册 service worker 的小启动惩罚,阻止在注册前对 bundle 进行重新优先级排序。

幸运的是,使用 service worker 并不是预取和缓存客户端 JavaScript bundle 的唯一方式,我们找到了一个更好的替代方案。

新特性:更好的 JavaScript bundle "缓冲"方式

在 Qwik 1.14 中,我们放弃了使用 service worker,转而采用利用 <link rel="modulepreload"> 的解决方案作为新的默认选项。

该解决方案包含一个名为"Qwik Preloader"的小脚本。一旦它在客户端下载完成,它会将 <link rel="modulepreload" href="my-bundle.js#segment123456"> 添加到 html 的 <head> 中,这告诉浏览器预加载和缓冲相应的 JavaScript bundle。

通过这样做,我们不仅消除了任何启动惩罚,而且一旦为交互预加载了 bundle,即使在 CPU 性能较差的设备上,Qwik 组件的任何交互也能绝对即时响应 🚀

不再有旧/低端设备上的首次交互延迟

在我们对一台相当用旧的小米 Note 7 Pro 的测试中,延迟仅达到约 100 毫秒,几乎不被人眼察觉。这意味着只有非常旧或使用过度的设备才会受到这个瓶颈的影响,但这仍然是我们想要改进的一个领域。

在屏幕录制中很难感受到,但如果你仔细观察上面的两个录像,你会注意到使用 service worker 时,Accordion 组件的第一次交互稍微慢一些。而使用 modulepreload,所有交互都是即时的。(你可能需要将视频设置为 0.5 倍速以更好地看到差异。)

在 CPU 较慢的设备上,使用 service worker 会出现这种延迟的原因是,从 CacheControl 中检索一个块并不总是只需要 1 毫秒,有时需要 10 毫秒或更多。例如,在第一次点击时检索 15 个 bundle,一个旧设备总共可能需要 150 毫秒或更多,而不是 15 毫秒。一旦 bundle 被执行并存在于浏览器内存或磁盘缓存中,交互就会即时进行。

相比之下,modulepreload 预加载的 bundle 由浏览器下载和编译。它们存在于内存中,并准备好在请求时立即执行。因此,如果交互所需的所有 bundle 都已预加载,交互将是即时的。没有网络请求,没有 service worker 拦截,没有从 CacheStorage 检索 bundle。

这里有一个小 Excalidraw 图来说明区别:

image.png

不再有启动延迟

Service Worker 必须先注册才能进行任何预取。这不仅对 TTI 和 TBT(总阻塞时间)有一定的负面影响,而且如果用户在注册前进行交互,还会阻止 Qwik 重新排序 bundle 的优先级。

image.png 使用 modulepreload,我们现在可以在 html 渲染后立即开始预加载 bundle。默认设置确保 FCP(首次内容绘制)LCP(最大内容绘制)保持尽可能快,并略微改善了相比 service worker 的 TTI 和 TBT 分数。

基于启发式的更好优先级排序

Qwik service worker 中已经存在的一个强大功能,在 Qwik Preloader 中也实现了,就是能够在用户交互时"重新排序" bundle 的优先级。如果某些 bundle 在预加载队列的底部,我们可以告诉浏览器立即开始预加载它们。这意味着无论如何,TTI 保持恒定,不会随着需要加载的 bundle 数量和大小成比例增加。

但如果框架能够提前知道按什么顺序"缓冲"什么内容,那就更好了。Qwik 现在可以开箱即用地根据用户最可能首先交互的内容来猜测哪些 bundle 更可能被请求。

这并不完美(为此我们有 Qwik Insights),但比以前好多了!Qwik 现在给"用户事件"或"首屏"组件等更重要的内容更高的"分数",而对它认为不太重要的内容给予较低的优先级。

例如,Qwik 假设导航栏上首屏的搜索输入框比远低于首屏的页脚上的按钮更可能更早被使用,但如果一个在非常慢的网络上的用户滚动并点击页脚上的按钮,Qwik 将在搜索输入框之前重新排序其 bundle 的优先级,以便按钮代码尽快执行。

更快的 CI 时间

由于上述性能改进,我们在 qwik.dev 上的完整 CI 测试现在运行时间约为 10 分钟,而不是之前的约 15 分钟,其中大部分可归因于 E2E 测试,这些测试在浏览器中进行了许多渲染,因此启动了大量 service worker。

如何获得所有这些好处?迁移步骤

迁移到以 Qwik Preloader 为新默认选项的 Qwik 1.14.0 不应该需要你付出太多努力,但有几个变化你应该了解。

目前 93% 的浏览器支持"modulepreload",这在我们开始使用 service worker 进行"缓冲"时并非如此。对于不支持的设备,preloader 将回退到 <link rel=preload>,这不会提前编译代码,但仍然有效,并将支持率提高到 100%。实际上,你可以放心,无论用户使用什么设备,都能体验到闪电般的速度。

以下是你需要遵循的步骤:

1. 注销 service worker

service worker 不再使用,但为了确保它从用户的浏览器中卸载(以防止它使你的应用变慢),我们建议等待几周/几个月后再从根组件中移除 <ServiceWorkerRegister />

qwik-city service worker 和实验性的 Qwik prefetch service worker 都已更新,可以自动为你完成这项工作。

所以暂时不要从你的应用(在 root.tsx 中)中移除 service worker 注册。等到你的用户至少加载过一次你的网站,或者足够长的时间让他们的浏览器清除了站点数据。

2. 重要:配置你的 Cache-Control 头

由于 service worker 不再强制缓存 bundle,使用云提供商推荐的方式设置适当的 HTTP 缓存头很重要,例如使用 Vercel 的 vercel.json 或 Netlify 的 netlify.toml。

bundle 和资源通常分别在 /build/assets 提供。它们的文件名包含基于内容的哈希值,使它们不可变。如果它们改变,它们的名称也会改变。因此,你可以为这些设置长期缓存头。

推荐的头是:

Cache-Control: public, max-age=31536000, immutable