|
背景:Builder.io的产品专注于电子商务,而电子商务热爱速度!感官上提升速度需要考虑的两个维度:FCP和TTIFCP(FirstContentfulPaint,首次内容绘制)当浏览器第一次渲染任何文字、图片,以及非空白的canvas或SVG的时间产物:SSRTTI(TimetoInteractive,用户可交互时间)用于描述页面何时包含有用的内容,并且主线程何时空闲并且可以自由响应用户交互,包括注册事件处理程序。产物:Qwik简介wik是一个以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(可恢复)的概念,启动时则不需要这个补水的过程,也就大大缩减了客户端的启动时间。0Resumable:指服务器暂停执行并在客户端恢复执行,而无需重新构建和下载所有应用程序逻辑。为了实现这一点,Qwik需要解决3个问题:侦听器、组件树、应用程序状态侦听器:现有框架通过下载组件并执行来收集事件侦听器,然后将这些事件侦听器附加到DOM上。当前的方法存在以下问题:1、需要快速下载模板代码。2、需要立即执行模板代码。3、需要急切地下载事件处理程序代码。以上问题,会随着业务越来越复杂,造成代码量越来越大,从而对性能产生影响。Qwik则通过将事件侦听序列化到DOM中+Qwikloader来解决上述问题click meQwik仍然需要收集侦听器信息,但是这一步放到服务器去完成,将其序列化成HTML,以便后续进行恢复。on:click属性包含恢复应用程序的所有信息,该属性告诉Qwikloader要下载哪个代码块以及从该块中执行函数名。渲染首屏中,在HTML中会插入侦听器的核心代码Qwikloader,小于1kb,将在1ms内执行,首次渲染只有这一段js,使得首屏速度接近纯HTML页面,也是Qwik页面在PageSpeedInsights上得分将近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 ( Count: {counter.coun} counter.coun++}>+1 );});页面效果:01、先看语法1、$后缀,表示懒加载该函数2、useStore状态管理3、Hooks:useServerMount、useClientEffect...等等可以看出整体结构其实和React还是很类似的,只是提供了很多自己独特的api,上手成本可以说不高~2、HTML
Welcome to Qwik City
这里是通过renderToStream函数生成的HTML我们可以看到里边包含了1、Qwik特有属性q:id、q:container、q:slot、q:host、on:click等等2、script代码块qwik/json3、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进行prefetch4、页面渲染我们继续看计数器这个例子点击后+10其中点击事件代码: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/json2、通过解析json创建state3、获取container的state4、创建反序列化Dom属性工具函数5、启动代理Proxy,实现get、set的发布订阅6、重建state7、触发set,触发render通过以上例子,我们基本了解了Qwik实现的原理。最后:我们可以看出,Qwik的优点还是很明显的,通过更加细粒的代码,以及事件委托来大大缩短了首次可交互时间,在渲染上,也充分利用了dom的属性,使组件可以独立渲染等等。但是也会存在一些争议的地方,像点击事件后,是否会下载代码失败,prefetch策略是否真得好用,等等问题。但是整体来说,还是一个很有前瞻性的框架的,也真正解决了一些现有的问题,如果有机会,针对页面首屏加载速度,首次交互要求很高的网页,是可以尝试一下的。好了,就先写到这里,如果有写的不对的地方,欢迎大家指正,共同进步~~~
|
|