追求极致性能的Qwik

发表于 1年以前  | 总阅读数:244 次

背景:

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(可恢复)的概念,启动时则不需要这个补水的过程,也就大大缩减了客户端的启动时间。

0

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>
    </>
  );
});

页面效果:

0

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、点击事件

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

0

内部代码:

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

0

其中点击事件代码:

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策略是否真的好用,等等问题。但是整体来说,还是一个很有前瞻性的框架的,也真正解决了一些现有的问题, 如果有机会,针对页面首屏加载速度,首次交互要求很高的网页,是可以尝试一下的。

本文由哈喽比特于1年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/105PKyG1mrwOMB07Z9ercg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:8月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:8月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:8月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:8月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:8月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:8月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:8月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:8月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:8月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:8月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:8月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:8月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:8月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:8月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:8月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:8月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:8月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:8月以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  236901次阅读
vscode超好用的代码书签插件Bookmarks 1年以前  |  7076次阅读
 目录