关注

“脚本之家

”,与百万开发者在一起

出处:魔术师卡颂(ID:gh_52d0bec584f9)

如若转载请联系原公众号

在前不久的WWC22中, builder.io 的CTO 「miško hevery」(同时也是 Angular / AngularJS 的发明者)发表了一段充满想象力的演讲。

miško hevery

在演讲中,他介绍了一款全栈SSR框架 —— Qwik ,这款框架号称 「能帮你移除项目中99%的JS代码」

他是如何办到的,本文我们来介绍下 Qwik 。

性能差?码农不背锅

先来聊聊 Qwik 诞生的背景。

对于很多 2C web 应用(比如电商),首屏性能指标关乎用户留存,用户留存关乎赚多少钱。

所以,应用打开速度会影响赚钱。

然而,对于前端开发者,首屏性能指标并不容易优化。究其原因,并不是开发者不够努力。

让我们来看两个性能指标。

如何优化FCP

FCP (First Contentful Paint,首次内容绘制)测量 「页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间」

当前 web 应用普遍采用 「前端框架」开发,这意味着会引入大量 JS 代码(框架本身代码、第三方依赖包的代码......)

从 HTML 开始解析到最终页面渲染,中间还要经历:

  1. 下载框架 JS 代码
  2. 执行框架 JS 代码
  3. 由框架完成页面渲染

这就导致 FCP 指标的下降。

为了优化 FCP ,框架作者提出了 SSR (Server Side Render,服务端渲染),在服务端生成首屏所需 HTML ,这就为 FCP 省去了上述三个步骤所需时间。

但是, TTI 指标仍然需要优化。

如何优化TTI

TTI (Time to Interactive,用户可交互时间)测量 「页面变得完全可交互所需时间」

主要衡量的是从下述1到3所需时间:

  1. 首先衡量 FCP 时间
  2. 为页面中的元素绑定事件
  3. 对元素产生交互后,事件响应时间在50ms内

使用 SSR 后,虽然 FCP 降低,但是框架 hydrate (注水,即框架使页面能够响应交互)所需时间对 TTI 会有影响。

可见,性能瓶颈的源头在 JS 代码。

React18 的 Selective Hydration 通过 「让用户交互的部分优先hydrate」来优化 TTI 指标。

但是, Qwik 更极端,他的目标是 —— 干掉所有不必要的 JS 耗时,这里的耗时包括两部分:

  • JS 作为静态资源加载的耗时
  • JS 运行时的耗时

超超超细粒度hydrate

如果说传统 SSR 的粒度是 「整个页面」

那么 React18 的 Selective Hydration 的粒度是 「产生交互的组件」

那么 Qwik 的粒度是 「组件中的某个方法」

举个例子,下面是 HelloWorld 组件(可以发现, Qwik 采用类似 React 的语法):

对应页面渲染效果:

打开浏览器 Network 面板,这个页面会有多少 JS 请求呢?

由于这是个静态的组件,没有逻辑,所以答案是:没有 JS 请求。

再来看看经典的计数器 Counter 组件,相比 HelloWorld ,增加了 「点击按钮状态变化的逻辑」,代码如下:

对应页面渲染效果:

打开浏览器 Network 面板,这个页面会有多少 JS 请求呢?

答案还是:没有 JS 请求。

注意这两个组件的代码中,定义组件使用的是 component$ ,有个 $ 符号。

在 Counter 中, onClick$ 回调也有个 $ 符号。

在 Qwik 中,后缀带 $ 的函数都是 「懒加载」的。

hydrate 的粒度有多细,就取决于 $ 定义的多细。

比如在 Counter 中, onClick$ 带 $ 后缀,那么点击回调是懒加载的,所以首屏渲染不会包含 「点击后的逻辑」对应的 JS 代码。

在点击按钮后,会发起2个 JS 请求,第一个请求返回的是 「点击后的逻辑」

第2个 JS 请求返回的是 「组件重新render的逻辑」

这两段代码执行后, Counter 变为1。

审查元素会发现,点击前, button on:click 属性中保存了 「逻辑所在的地址」

点击后,会从对应地址下载 JS 代码,执行对应逻辑。

从优秀到极致

是不是觉得已经优化到极致了?还没。

对于一些在页面中长期存在的、需要 JS 驱动的模块(比如轮播图),在模块展现前, 「模块对应JS」不是必要的。

比如下面这个钟的示例,页面中有个长长的列表,超过一屏高度,在列表底部有个钟。

下面是列表滚到底的样子:

在 Clock 组件的 useClientEffect$ 中定义 「时钟指针摆动的逻辑」

Qwik 中也存在类似 React 的 useEffect ,但在 Qwik 中这个 Hook 可以在服务端/客户端执行。

为了区分, useClientEffect 是 「只在客户端执行的useEffect」

加了 $ 后缀,代表他是 「懒加载的」

具体效果是:当页面滚动到钟露出之前, useClientEffect$ 对应 JS 代码都不会请求。

当钟露出后,会发起两个 JS 资源请求:

  • useClientEffect 的逻辑
  • Clock 组件重新渲染的逻辑

如果审查元素,在钟露出前,指针对应元素都是不动的:

当钟露出,加载并执行 JS 代码后,才开始执行动效:

对数据hydrate

在传统 SSR 中,数据其实被初始化了两次:

  1. 页面首次渲染,此时服务端导出的 HTML 中已经携带了首屏渲染的数据
  2. 框架 hydrate 后,数据再转化为框架内的状态供后续渲染

在 Qwik 中,页面初始化时会存在 type 为 qwik/json 的 标签用于存储 「当前页面中被激活的状态对应数据」

什么叫 「被激活」呢?

比如,下面是一篇文章的评论区,这是首屏渲染后的样子:

这些评论数据会出现在 qwik/json 保存的数据中么?

不会,因为没有交互激活他们。

我们发现,有一条评论被折叠了,点击后会展开这条评论:

点击这个行为会请求:

  • 点击逻辑对应的 JS 代码
  • 这条评论对应组件的重新渲染逻辑

此时,评论数据才会出现在 qwik/json 中,因为点击交互激活了这个数据。

所以在 Qwik 中,如无必要,数据不会被初始化两次。

HTML 中存在 「未激活的数据」, qwik/json 的 标签中保存了 「激活的数据」,这个特性会带来一个很有意思的效果:

复制调试工具中 「Elements面板下的DOM结构」后,再在新页面中粘贴,就能复现 「页面当前的交互状态」(比如,输入框内仍然保留之前输入的内容):

复制红框内的内容

换做其他框架,只能复现 「页面初始时的状态」

交互时再请求JS不会卡么?

有同学可能会问,如果在网络不好的情况下,交互时再请求 JS 代码不会让交互变得卡顿么?

Qwik 允许你指定 「哪些组件可能是用户大概率会操作的」(比如电商应用中,购物车按钮被点击的概率高)。

这些组件逻辑对应 JS 代码会 prefetch ,在不影响首屏渲染的前提下被预请求:

并且这些组件 prefetch 的顺序是可以调整的。

这意味着可以追踪用户行为,以 「用户交互的频率」为指标,作为组件 prefetch 优先级的依据,启发式提升应用性能。

这才是真正的 「以用户为导向」的性能优化,而且是全自动的。

总结

当今是个前端框架百花齐放的时代,不同框架都在寻找自己独特的卖点。

Qwik 的卖点是:将 JS 代码的拆分从常见的 「编译时」(比如 webpack 分块)、 「运行时」(比如 dynamic import ),变为 「交互时」

对 JS 代码的极致拆分,只为达到一个目的 —— 在首屏渲染时,移除你项目中99%的 JS 代码。

你觉得这波操作怎么样?

这是一件程序员才懂的T恤

7.8k Star!一个强大的 JS 代码混淆工具

67行JS代码实现队列取代数组,面试官刮目相看

能用js实现的最终用js实现,Shell脚本也不例外

如何移除你项目中99%的JS代码
标签: