Bobolo
  • Home
  • Me

Qwik Lazyload Serialize

Qwik 带来的新方式

15 Min Read

Thur Feb 23 2023

Written By Bobolo

Qwik,一个新的 Web 框架,它只需要加载大约 1kb 的 JS 作为 Loader(无论应用程序复杂性如何)即可,且不随程序复杂而影响性能。

考虑到它在用法上和现有框架的区别并不大,对于用法、命名、工具等就不做过多描述,而且版本太新也可能后续会有用法等的改动,但是思路和根本策略应该是不会改变。所以接下来主要是探讨它的实现原理和思路

它与 React 非常相似,无论是 JSX 还是 State,也有 React 没有的 Slots,但对使用过 React 的开发者还是很好理解

Why

要理解它的思路和原理,首先我们要看一下它要解决的问题,在这之前我们先简单回顾一下现有框架的 Render/Hydration 过程

  1. 从服务端获取 HTML
  2. 根据 HRML 下载所有的 JS
  3. 解析和执行 JS
  4. 恢复状态和绑定事件(Hydration)

Render

只有完成了 Hydration 后整个页面才算是可交互,而目前的框架中,大部份需要完成 Hydration 后才看得到界面,当然现在有部份框架采用了 Islands Architecture(孤岛架构)。

Hydration 需要完成的前面三步,Download Js 和 Parser and Executing Code 都非常耗时,尤其在界面复杂的情况下。而 Hydration 也同样耗时并且可能存在无意义,因为可能 Component 根本没有用户交互。

Qwik 的目标是即时加载界面,即使在移动端或者在弱网的状态下,这也是绝大部份框架性能优化需要考虑的点。因此怎么优化 2、3、4 就非常重要。

Strategy

优化界面加载速度可以有多种方式,例如优化网络、压缩体积,优化执行逻辑、缓存、Lazyload 等,但要想实现首次打开界面就即时加载,并不是每个都适用。

  • 网络优化,例如 CDN、HTTP 等是有帮助,但框架是无法改变网络本身
  • 压缩体积,无论是压缩 HTML、CSS、JS 都有效果,而随着应用的复杂,始终不可避免的增大
  • 优化执行逻辑,优化业务逻辑可以压缩体积和执行速度,框架本身的体积和水合的执行成本是无法优化
  • 缓存,缓存可以在后续加载和执行中有提升,对于首次加载帮助不大

Lazyload

Qwik 实现这个目标的根本策略是 Lazy,把 Lazy 极致化,把代码加载和执行 Lazy 达到一个比组件还细的纬度,Qwik 官网说的是两个策略 延迟 JavaScript 的执行和下载在服务端序列化框架和程序的状态和在客户端恢复,但感觉后面的序列化更像是为了实现 Lazyload 而采用的方法。

Lazyload 相信都不陌生,在使用 Router 或者一些 Click 事件中都有用到 Import.then 来做 Lazyload。

看起来可以解决代码体积和执行时间的问题,但如果 Qwik 本身体积太大也同样无法解决,而且 Lazy 后又要怎么把 Component 的 Event 和 State 进行 Hydration?

Serialize

大多数框架通过执行代码来构建状态,以引用和闭包的形式将这种状态保存在内存中。所以需要下载 JS 去做 Hydration。而 Qwik 的做法是将状态等信息保存在 DOM 的 Attribute 中。

引用和闭包都不是可序列化的,但作为字符串的 DOM 属性是可序列化的。这是 Resumable 的关键!

在 Server 把 State、Event 等数据,声明式的加到 Dom 的 Attribute(Qwik 构建)中,在 Client 就可以根据 Dom 的 Attribute 反序列化然后 Hydration。这样在 DOM 中保持状态的好处包括:

  • DOM 以 HTML 作为其序列化格式。以字符串属性的形式将状态保存在 DOM 中,可以在需要时 requestidlecallback、click、mouseover... 才反序列化。
  • 每个组件都可以独立于任何其他组件恢复。这种无序的 reHydration 只允许对整个程序的一个子集进行 reHydration,并限制需要下载的代码量作为对用户操作的响应。
  • 程序可以在任何时间点(不仅在初次 render 时)序列化,并且可以多次序列化。
  • 代码体积,Client 只要一个恢复状态和绑定时间的 JS (Qwik 1KB 实现),不需要 Components 和框架的其他 Code。

Implement

Lazy 和 Serialize 这两个策略已经确定,但需要具体去实现在 Server 序列化数据以及在 Client 中 Resumable。

先说序列化数据,首先需要确定应该包括那些代码模块、可以序列化的数据、怎么序列化和反序列化。

$ 符号

Qwik 用 $ 符号作为确定需要序列化代码块的标志,用户在开发时可以对需要进行序列化的代码加上 $,Qwik 本身也会有一些规则限制,例如 State 是需要在 component$ 中使用。

同时不是所有的代码都可以加上 $,一是不是任何代码都可以序列化,二是涉及 Resumable 的代码才值得需要加上。

<button onClick$={() => state.counter++}>Increment counter</button>
// or
import { $ } from '@builder.io/qwik'
const handleClick =$(() => state.counter++)
<button onClick={handleClick}>Increment counter</button>

因此在开发的过程中,用户就已经在 Qwik 的限制下把标志加上,不在 Qwik 规则但需要序列化的代码可以自己加上。

感觉 $ 符号可能会改变,首先语意不明确,而且在开发过程中,各种 Api 后面加 $ 也显得很奇怪

Serialization

在可以确定需要序列化的代码块后,就需要确定可以序列化的代码是什么。代码块或者说 State 的数据存在多种情况,如 Promise、Date、Url 等对象。

序列化的最简单方法是JSON.stringify,但无法解决上面提到的问题,因此需要实现一个序列化的工具。

而序列化的代码块或者 State 等数据过于复杂,很有可能导致 Dom 属性的 Value 非常的长,无论是反序列化或者 Render 都变得困难,本着 Lazy 的理念,这些数据都应该 Dynamic Import ,所以 Dom 的 Value 更应该是一个可以 Import 代码块的 URL。

并非所有数据都需要 Dynamic Import,如简单的 base path、version 等是不需要

QRL

QRL(Qwik URL)是一种特殊形式的 URL,Qwik 使用它来延迟加载内容。

Lazyload 的代码块, 有可能需要执行其中的 function,所以除了需要的 path 之外,还需要 functionname 以及参数。参数因为类型多,所以以数组索引的形式编码在 QRL 中 q:obj

 ./path/to/chunk.js#SymbolName[0,2]

 qrl('./chunk.js', 'Counter_onClick', [count, props])

Optimizer

确定了需要优化的代码,也确定了优化后的形式,接下来的就是把加上 $ 的代码块转换成 QRL。Qwik 实现了一个 Optimizer,用来查找 $ 并提取 $ 符号后的表达式,将其转换为可延迟加载和可导入的代码块。

Optimizer 用 Rust(作为 WASM 来使用) 开发,并且需要和 Runtime 结合使用。

Build

Render

上面的 Qwik 代码在经过 build 之后生成下面的 HTML

Render

可以看到生成的 HTML 中有很多带有 q: 以及 on: 的属性,这些属性就是经过 Qwik 处理的信息。在点击 button 之后会加载 q-710572fb.js

还有很多 Qwik 生成的属性没列出来,主要因为版本迭代快,很容易会更新,而且和原理没有绝对的关联

Render

加载完 Js 后会执行 s_uBvN6X0Vg3Y,即是 counter++,执行 counter++ 后会因为 Reactivity(Proxy)rerender 相关的组件。

QwikLoader

QwikLoader,用于在 Client 恢复状态和 Components 执行 Lazyload 的事件。整个 Loader 的流程如下:

qwik-loader

建议直接看源码,代码不到 200 行,而且不会有太大改动。

Perception

Qwik 将 Lazy 作为最主要策略并发挥到极致,JS 的 Size、Code parser、还有 Hydration 都做到了 Lazy,这是与其他框架所不同的地方。它对于首次加载上是比较有优势,如果对首次加载并没有非常高要求,那它也不是绝对的领先。

Lazy

Lazy 对于 Qwik 就和 Vdom 对于 React 一样,React 是在运行时优化执行的性能,Qwik 是减少 JavaScript。并不是说 Qwik 比现有框架好,而是它不依靠高效的算法,而从根本上 Lazy 可能不执行的代码。

但过多 Download JS 导致网络波动上是否更容易出现失败的问题,应该把 JS 拆分到什么颗粒度,而 JS 之间的执行顺序是否也是一个问题,又怎么去优化下载?

执行顺序和打包的策略在 Optimizer,因为对 Rust 不是非常熟悉就没详细去看,后续可能会研究一下。

Qwik 使用 Web WorkerService Worker预取(Prefetch)JS 以及缓存。

网络部份可能和 HTTP 的对头阻塞(Head-of-Line blocking) 一样处理,Qwik 优化点更偏重网络,可能很多网络策略和优化都可以应用

声明式的未来

声明式的将数据、状态放到 HTML 中是一个优化性能的做法,可以减少代码体积和执行时间,而且如 Shadow Dom 也在做声明式,具体可以看 Web Compoensts

随着 SSR 在项目中使用的增多,性能、数据传输都变得更重要,声明式是个不错的处理思路。目前 Shadow Dom 和 Qwik 都采用这种方式,但浏览器本身不支持 Qwik 这些框架,需要自己做反序列化。

Qwik 更像一种思想,现有框架可以学习它的 Lazy 、声明式以及加载策略,类似于 React 的 Scheduler,Qwik 也开源了延迟加载的库 partytown,感觉现有框架可以考虑实现部份 Lazy 来优化性能。

Conclusion

比较推荐去学习它的核心思想,至于一些用法和实现细节可以等相对稳定后再去研究。如果想在复杂项目中使用,还是要考虑一下成本。

虽然使用 JSX、还有和 React Hooks 类似的语法,也还是需要去学习和掌握。而且目前版本太新,可能迭代很快增加维护成本。目前社区也不是很强大,一些可能用到的 State Managerment、UI Components、Tools 都可能没有。更推荐在自己项目中去使用,这个 Blog 就是用 Qwik 来构建的。

Reference

Powered by Bobolo

Copyright © Bobolo Blog 2021