背景:

Builder.io的产品专心于电子商务,而电子商务热爱速度!

感官上提升速度需求考虑的两个维度:FCP和TTI

FCP(First Contentful Paint,首次内容绘制)当浏览器第一次烘托任何文字、图片,以及非空白的 canvas 或 SVG 的时刻

产品:SSR

TTI(Time to Interactive,用户可交互时刻)用于描绘页面何时包含有用的内容,而且主线程何时闲暇而且能够自由呼使用户交互,包含注册事情处理程序。

产品:Qwik

简介:

Qwik是一个以 DOM 为中心的可康复 Web 结构,旨在完成最佳的交互时刻,专心于可康复性和代码的细粒度推迟加载的SSR结构。

Qwik在服务器上开始履行,序列化HTML,发送给客户端。序列化后的HTML中,除了包含qwikloader.js(1kb)以外,不包含任何js的加载及履行。当用户进行交互后,恳求下载相应的交互代码,Qwik从服务器中止的当地康复履行。

方针:

Qwik 的方针是供给即时使用程序,Qwik 经过两个首要战略完成了这一点:

1、尽可能长期地推迟 JavaScript 的履行和下载。

2、在服务器端序列化使用程序和结构的履行状况,在客户端康复。

剖析:

Qwik 速度快不是因为它使用了聪明的算法,而是因为它的规划办法使得大多数 JavaScript 永远不需求下载或履行。它的速度来自于不做其他结构有必要做的事情(例如水合作用-hydration)。

比较:

现有的SSR/SSG 使用在客户端启动时,它需求客户端上的康复三条信息:

1、侦听器 – 定位事情侦听器并将它们安装在 DOM 节点上以使使用程序具有交互性;

2、组件树 – 结构数据并表现在组件树上。

3、使用程序状况 – 康复使用程序状况。

这被称为水合作用。当时一切结构都需求此步骤以使使用程序具有交互性。

这个补水进程能够说是很昂贵的,首要因为以下两点:

1、结构有必要下载与当时页面相关的一切组件代码。

2、结构有必要履行与页面上的组件相关的模板,以重建侦听器方位和内部组件树。

而Qwik则不同,Qwik提出Resumable(可康复)的概念,启动时则不需求这个补水的进程,也就大大缩减了客户端的启动时刻。

寻求极致功能的Qwik

Resumable:

指服务器暂停履行并在客户端康复履行,而无需从头构建和下载一切使用程序逻辑。

为了完成这一点,Qwik需求处理3个问题:侦听器、组件树、使用程序状况

侦听器:

现有结构经过下载组件并履行来收集事情侦听器,然后将这些事情侦听器附加到 DOM上。

当时的办法存在以下问题:

1、需求快速下载模板代码。

2、需求立即履行模板代码。

3、需求急迫地下载事情处理程序代码。

以上问题,会跟着事务越来越杂乱,造成代码量越来越大,从而对功能产生影响。

Qwik则经过将事情侦听序列化到 DOM 中+Qwikloader来处理上述问题

<button on:click="./chunk.js#handler_symbol">click me</button>

Qwik 依然需求收集侦听器信息,可是这一步放到服务器去完成,将其序列化成HTML,以便后续进行康复。

on:click 特点包含康复使用程序的一切信息,该特点告诉 Qwikloader 要下载哪个代码块以及从该块中履行函数名。

烘托首屏中,在HTML中会刺进侦听器的中心代码Qwikloader,小于 1kb,将在 1ms 内履行,首次烘托只要这一段js,使得首屏速度挨近纯HTML页面,也是Qwik页面在 PageSpeed Insights 上得分 将近100 分的原因。

组件树:

现有结构,假如组件边界信息已被损坏,则需求从头下载组件模板并履行补水,Hydration 的本钱很高,所以功能也会受到损失。

Qwik会将该组件信息序列化为 HTML,则能够

1、在组件代码不存在的情况下重建组件层次结构信息,组件代码能够坚持惰性。

2、Qwik 只能为需求从头烘托的组件而不是一切预先烘托的组件推迟履行此操作。

3、Qwik 收集store和组件之间的联系信息,并创立一个订阅模型,告诉 Qwik 哪些组件由于状况更改而需求从头烘托。订阅信息也被序列化到 HTML。

使用状况:

一切结构都需求坚持状况。大多数结构以引用和闭包的方式将此状况保存在 JavaScript 堆中,这样就导致初始化时候需求下载一切模板,做好相关,可是这样一般会有个问题,便是假如需求康复子组件,那父组件也需求康复。Qwik的共同之处在于状况以特点的方式保存在 DOM 中,这使得Qwik组件能够独立进行康复。

在 DOM 中坚持状况的结果有许多共同的长处,包含:

1、经过以字符串特点的方式在 DOM 中坚持状况,使用程序能够随时序列化为 HTML。

HTML 能够经过网络发送并反序列化为不同客户端上的 DOM。然后能够康复反序列化的 DOM。

2、每个组件都能够独立于任何其他组件来康复。这种只允许对整个使用程序的一个子集进行再水化且无序,并需求下载以呼使用户操作的代码量,这与传统结构有很大不同。

3、Qwik 是一个无状况结构(一切使用程序状况都以字符串的方式存在于 DOM 中)。无状况代码易于序列化、传输和康复。这也是允许组件彼此独立再水合的原因。

4、使用程序能够在任何时刻点进行序列化(不仅仅是在初始烘托时),而且能够屡次序列化。

原理简析:

咱们经过完成一个计数器,来剖析一下

环境:node14

代码:

import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
  const counter = useStore({ coun: 0 });
  useServerMount$(() => {
    console.log("服务器履行");
  });
  useClientEffect$(() => {
    console.log("客户端履行");
  });
  return (
    <>
      <div>Count: {counter.coun}</div>
      <button onClick$={() => counter.coun++}>+1</button>
    </>
  );
});

页面作用:

寻求极致功能的Qwik

1、先看语法

1、$后缀,表示懒加载该函数

2、useStore 状况管理

3、Hooks: useServerMount、useClientEffect…

等等

能够看出整体结构其实和React仍是很相似的,仅仅供给了许多自己共同的api,上手本钱能够说不高~

2、HTML

<html q:version="0.0.39" q:container="paused" q:host="" q:id="0" q:ctx="qc-c qc-ic qc-h qc-l qc-n" q:base="/build/">
<head q:host="" q:id="1">
<meta q:head="" charset="utf-8">
<link q:head="" rel="canonical" href="http://localhost:5173/">
<style q:style="s87awj-0">
    header {
      background-color: #0093ee;
    }
</style>
<link rel="stylesheet" href="/src/global.css">
</head>
<body q:host="" q:id="2">
    <div q:key="haiwfuvnx7g:" q:id="3" q:host="">
    <div q:key="Li90Ltjk0Is:" q:id="4" q:host="" q:sref="p">
    <main>
        <q:slot q:sref="p">
            <div q:key="buH6QBbKJm4:" q:id="7" q:host="">
                <h1 q:id="8" on:click="/src/routes_component_host_h1_onclick_a0y0gxm29ey.js#routes_component_Host_h1_onClick_A0y0gXM29EY">
                Welcome to Qwik City
                </h1>
            </div>
        </q:slot>
    </main>
<script type="qwik/json">
    {
      "ctx": {},
      "objs": [],
      "subs": []
    }
</script>
<script id="qwikloader">
    (() => {
        ...
    })();
</script>
</body>
</html>

这儿是经过renderToStream函数生成的HTML

咱们能够看到里面包含了

1、Qwik特有特点q:id、q:container、q:slot、q:host、on:click等等

2、script代码块qwik/json

3、script代码块qwikloader

其间qwikloader包含了侦听器中心逻辑,其他特点则是用来反序列化,进行烘托组件树和处理状况时用。

3、点击事情

点击按钮后:这儿仅仅展示了一个打印函数,和本例无关,本例代码在下边再说~

寻求极致功能的Qwik

内部代码:

export const routes_component_Host_h1_onClick_A0y0gXM29EY = ()=>console.warn('hola');

能够看到里面便是咱们写的履行函数~

这一步首要是经过html内的Qwikloader.js来完成的

中心原理便是经过事情委托来监听一切事情,当点击时,获取当时dom上的特点,进行规矩解析,然后import加载进来

const dispatch = async (element, onPrefix, eventName, ev) => {
            element.hasAttribute('preventdefault:' + eventName) &&  // preventdefault:click
              ev.preventDefault()
            const attrValue = element.getAttribute(      // 获取on-document:click 特点
              'on' + onPrefix + ':' + eventName // on-document:click
            )
            console.log('dispatch获取当时元素'+'on' + onPrefix + ':' + eventName+ '事情特点值', attrValue)
            if (attrValue) {  // 存在on:click 特点
              for (const qrl of attrValue.split('n')) {
                console.log('特点上原url', qrl)
                const url = qrlResolver(element, qrl) // 是否自定义域名
                console.log('处理后url', url)
                if (url) {
                  const symbolName = getSymbolName(url)
                  console.log('symbolName-hash值', symbolName)
                  console.log('引进js途径', url.href.split('#')[0])
                  const handler =
                    (window[url.pathname] ||
                      findModule(await import(url.href.split('#')[0])))[    // 引进js
                      symbolName
                    ] || error(url + ' does not export ' + symbolName)
                  const previousCtx = doc.__q_context__
                  if (element.isConnected) {    // 现已刺进dom
                    try {
                      doc.__q_context__ = [element, ev, url]
                      handler(ev, element, url) // 履行引进的js
                    } finally {
                      doc.__q_context__ = previousCtx
                      emitEvent(element, 'qsymbol', symbolName)
                    }
                  }
                }
              }
            }
          }

这儿我想咱们也会有个疑问:假如网络推迟,点击事情会不会卡顿呢?

下边说下Qwik是怎样处理的,官方文档仅仅说Qwik自己做了一些优化战略,可是没有细说。

我简单看了下,Qwik是用了html的prefetch,对要加载的js文件进行了预加载,这样尽量保证点击前现已加载完js代码,又不影响主程序的加载

在options里有个prefetchStrategy的配置,能够自定义配置相应的url进行prefetch

4、页面烘托

咱们继续看计数器这个比如

点击后+1

寻求极致功能的Qwik

其间点击事情代码:

import { useLexicalScope } from "/node_modules/@builder.io/qwik/core.mjs?v=d5d641c1";
export const _id__component__Fragment_button_onClick_yirrteWPaW0 = ()=>{
    const [counter] = useLexicalScope();
    return counter.coun++;
};

能够看到,咱们源代码中的useStore会被转化成useLexicalScope,而且下载运行时的core.mjs

在core.js内会履行康复, 首要逻辑在resumeContainer函数内,以下为删减后代码

const resumeContainer = (containerEl) => {
    // 康复
    const doc = getDocument(containerEl);
    const isDocElement = containerEl === doc.documentElement;
    const parentJSON = isDocElement ? doc.body : containerEl;
    const script = getQwikJSON(parentJSON); // 获取qwik/json数据
    script.remove();
    const containerState = getContainerState(containerEl);
    const meta = JSON.parse(unescapeText(script.textContent || '{}'));
    const getObject = (id) => {
        console.log('getObject值', id, getObjectImpl(id, elements, meta.objs, containerState))
        return getObjectImpl(id, elements, meta.objs, containerState);
    };
    const parser = createParser(getObject, containerState);    // 反序列化Dom特点东西函数
    // 启动代理,和Vue相似,经过修正get和set函数来完成发布订阅
    reviveValues(meta.objs, meta.subs, getObject, containerState, parser);
    // 重建当时state的obj
    for (const obj of meta.objs) {
        reviveNestedObjects(obj, getObject, parser);
    }
    Object.entries(meta.ctx).forEach(([elementID, ctxMeta]) => {
        const el = getObject(elementID);
        assertDefined(el, `resume: cant find dom node for id`, elementID);
        const ctx = getContext(el);
        const qobj = ctxMeta.r;
        const seq = ctxMeta.s;
        const host = ctxMeta.h;
        const contexts = ctxMeta.c;
        const watches = ctxMeta.w;
        if (qobj) {
            console.log('推送的啥', ...qobj.split(' ').map((part) => getObject(part)))
            ctx.$refMap$.$array$.push(...qobj.split(' ').map((part) => getObject(part)));
        }
        if (seq) {
            ctx.$seq$ = seq.split(' ').map((part) => getObject(part));
        }
        if (watches) {
            ctx.$watches$ = watches.split(' ').map((part) => getObject(part));
        }
        if (contexts) {
            contexts.split(' ').map((part) => {
                const [key, value] = part.split('=');
                if (!ctx.$contexts$) {
                    ctx.$contexts$ = new Map();
                }
                ctx.$contexts$.set(key, getObject(value));
            });
        }
        // Restore sequence scoping
        if (host) {
            const [props, renderQrl] = host.split(' ');
            assertDefined(props, `resume: props missing in q:host attribute`, host);
            assertDefined(renderQrl, `resume: renderQRL missing in q:host attribute`, host);
            ctx.$props$ = getObject(props);
            ctx.$renderQrl$ = getObject(renderQrl);
            console.log('ctx', ctx)
        }
    });
    directSetAttribute(containerEl, QContainerAttr, 'resumed');
    emitEvent(containerEl, 'qresume', undefined, true);
};

首要逻辑为:

1、获取html中的qwik/json

2、经过解析json创立state

3、获取container的state

4、创立反序列化Dom特点东西函数

5、启动代理Proxy,完成get、set的发布订阅

6、重建state

7、触发set,触发render

经过以上比如,咱们基本了解了Qwik完成的原理。

最后:

咱们能够看出,Qwik的长处仍是很明显的,经过愈加细粒的代码,以及事情委托来大大缩短了首次可交互时刻,在烘托上,也充分利用了dom的特点,使组件能够独立烘托等等。可是也会存在一些争议的当地,像点击事情后,是否会下载代码失利,prefetch战略是否真得好用,等等问题。可是整体来说,仍是一个很有前瞻性的结构的,也真正处理了一些现有的问题, 假如有时机,针对页面首屏加载速度,首次交互要求很高的网页,是能够测验一下的。好了,就先写到这儿,假如有写的不对的当地,欢迎咱们指正,共同进步~~~