本文译者为 360 奇舞团前端开发工程师
原文标题:JavaScript on Demand: How Qwik Differs From React Hydration
原文作者:Paul Scanlon
原文地址:https://thenewstack.io/javascript-on-demand-how-qwik-differs-from-react-hydration/
在本文中,我将深入介绍 Resumability
(可恢复性),这一 Qwik 用来解决繁杂的客户端 JavaScript 问题的技术。我还将说明这和 React 的处理方式有何不同。
QwikSchool.com 上有一个很棒的 Qwik 视频入门课程,教程会涵盖我在本文中讲的一些东西。但对我来说,阅读是更适合我的学习方式,使用类比的方式学习,我就能学的更快。所以,我会在本文中用「酒吧」和「啤酒」来做些类比,让你更好理解。
想象一下,你走进一间酒吧坐下,但酒保没有让你点单,而是直接拿了个大杯子,倒进去 6 品脱(品脱 pint 是英式和美式体系中常用的体积单位)啤酒。当酒保确认杯子确实能装 6 品脱酒后,他就会把酒全倒掉。然后,酒保会递给你(客户端)一个空杯子,再给你倒上 6 品脱酒。
你最终需要为这 12 品脱的酒付钱,而其中 6 品脱是被倒掉的,也就是说其中一半其实是你当时不需要的。
React Hydration 就是这么工作的。
想象一下,你走进一间酒吧坐下,酒保会让你点单。你说:”请给我一品脱啤酒“。酒保接着会将一品脱啤酒倒入一个正常大小的杯子,并递给你。
你将为这一品脱酒付钱。简而言之这就是 Qwik Resumability。
从顶层去看,你会发现,相比于 Qwik,React 的处理方式存在着巨大的性能浪费,且没有一个非常精准的方法来确定什么时候需要进行处理,需要处理多少。
我们继续使用前文提到的例子,酒保把 6 品脱啤酒倒入一个大杯子,然后全倒掉,最后递给你(客户端)一个空杯子,再重新倒酒。
React 在服务器上进行渲染时,就会发生这样的事。应用程序在服务端构建,之后被丢弃。服务器会把 HTML 发送到客户端,随后发送一大段 JavaScript 代码,这些代码是用于在客户端(浏览器)上重现服务端渲染的过程。如果在重现过程中发生什么异常,导致失败,你会在控制台看到 Hydration 的相关错误信息。
服务器需要构建一次页面,如此它才能知道自己要向客户端发送什么东西,这就解释了 React Hydration 重现 过程中的一半。但也许你会问,为什么在我啤酒的比喻里,6 品脱是一次倒出来的?答案就是「路由分块」。
不同版本的 React 框架可能会略微有些不同,但总的来说「路由分块」是在描述这样一种技术:服务器在对 React 应用进行构建时,框架会基于用户正在查看的页面路由,找出所需的 JavaScript。例如,如果用户正在访问 /dashboard
路由,那他需要的 JavaScript 和访问 /settings
路由的用户是不同的。页面路由会决定哪些 JavaScript 应该被包含在分块里。这技术真是太智能了,不过我们还能做的更好。
Qwik 的处理方式颗粒度更细。它不使用基于用户正在访问路由来决定要包含哪些 JavaScript 的方法,它把 JavaScript 分成小的多的多的小块。这些更小的块可以被更加快的发送到客户端。不仅如此,Qwik 能够查明用户正在访问给定路由的行为,并以此提供这些更小的 JavaScript 块。
在 Qwik 库中,不管是什么都带着 $ 后缀。比如,下面这个简单的组件,我们用 component$()
把他包起来了。
import { component$, $ } from '@builder.io/qwik';
const SimpleQwikComponent = component$(() => {
const handleClick = $(() => {
console.log('Hello world!');
});
return (
<div>
<p>Hello, I'm a simple Qwik component</p>
<button onClick$={handleClick}>Click me</button>
</div>
);
});
export default SimpleQwikComponent;
多亏了 $ 语法,Qwik 能容易的对分块边界进行优化,这样一来就能构建出更小的独立 JavaScript 分块。
另外,这也有助于在根据需要时提供这些小 JavaScript 块。
以 onClick$
为例。这段代码实际上仅在用户点击按钮时是需要的。如果用户没有点击按钮,这段在控制台输出 Hello world! 的 JavaScript 将仅存在于浏览器缓存(已由 Qwik 的 service worker 拉取),它不会被优先下载,而是会等到实际需要的时候在下载。
换一个例子,想想看,如果有一个大型应用程序,它有着很多不同的交互区域,你是否觉得:「所有用户在任何时候都需要全量的 JavaScript」?也许,一些用户某些时候只是需要其中一部分 JavaScript,你觉得呢?
事情变得有趣起来了。你可能听说过 Partytown,这是 Qwik 的创建者做的一个开源库,它能帮你把一些客户端脚本(比如 Google Analytics)加载转移到 service worker 上。通过将不必要的脚本转移到 service worker 上,我们解放了浏览器主线程,我们的应用程序可以更有效率的载入自己的代码,让应用程序表现的更出色。
不过...
默认情况下,Qwik 利用 service worker 来载入你的应用程序代码!那么说不定,从另一个角度看,这意味着你可以继续在主线程载入 Google Analytics,但这么做对性能的影响应该也很有限。🤷
一旦通过 service worker 载入 JavaScript,JavaScript 就会被浏览器缓存,所以下次用户访问时可能会重用已经下载的代码,Qwik 会从这些缓存中载入,而不是从网络。现在我的啤酒类比法不太好说明这个问题,但... 希望你能理解这些点。
举个例子,在我的站点上,每一个「帖子」页面都有一个 Reactions(反馈)组件。
就像下面这样:
这个组件负责这些工作:
Qwik 的处理策略非常智能。
让我说说 Qwik 厉害在哪里。下面是我那篇帖子的 network 选项卡截图:
到目前为止,仅有差不多 19kb 的 JavaScript 在页面上载入了(这是 Qwik 的核心代码)。
我们向下滚动一些,当 Reactions 组件进入视口,Qwik 会载入所需的独立 JavaScript 分块,即用来展示那些笑脸的分块,并启动一个客户端 GET 请求来获取。
不过等等,还没结束哦。
和 Reactions 组件进行交互(这会导致发送一个 POST 请求)后,你会发现 Qwik 又一次载入了所需的 JavaScript 分块,用于让按钮点击逻辑生效。
这些分块非常小,在我这个例子中,大部分尺寸都小于 1kb!
这就是 Qwik 的思路。只在所需的时间,载入所需的 JavaScript。对于像 Reactions 这样的组件来说,这一点尤为重要。Reactions 组件位于一个基本上可以被视为「静态页面」的页面底部。这意味着该页面不需要从服务器端拉取数据或进行服务器端渲染,因此其加载速度会比需要服务器端渲染(SSR)的页面更快。然而,尽管 Reactions 组件位于这样的静态页面中,它仍然需要执行 GET 和 POST 请求。
大部分时候,我帖子的读者不会滚动到页面最下方,因此他们确实不需要页面那部分的任何数据,不需要载入那部分的 JavaScript。不过,如果读者要向下滚动,Qwik 会介入并智能的选择「何时载入」、「载入什么」。太有才了!
当然,可能用 React 也能达成类似的效果吧。你可以自己整一个 intersection observer,并把它和 React 的 lazy / suspense 技术结合起来,这样数据(以及交互所需的 JavaScript)获取仅会在组件滚动到视口的时候进行,不过呢,这些东西都需要你作为一个开发者去考虑和优化,Qwik 可是替你把这些东西都考虑过了!
虽然酒吧和啤酒的类比可能看起来有点奇怪,但我相信在实际应用中,这个理论是站得住的:「你/用户」只应该在 需要的时候 点那些 你需要的 并 为之付钱。
老实说,应用程序代码之间有什么不同呢?无端地抛弃一半,不经询问的过度交付、过度收费,这真的没什么道理。讲真,我们只希望简简单单为那些我们点的东西付钱。
Qwik 团队撰写了一份技术说明文档,你应该去读一读:Resumable vs. Hydration。如果你想对 Qwik 感兴趣,那你应该了解一下!我在学习 Qwik 的过程中非常享受,并且我经常惊讶于不需要考虑太多,就能获得如此出色的性能。