乐趣区

关于javascript:Qwikjs框架是如何追求极致性能的

背景、

    Qwik是一款语法 ” 靠近 ”react的前端 ssr 框架, 前段时间看了两篇 Qwik 相干的文章, 对这个框架有了些趣味, 然而去网上搜了一下, 发现相干的中文文章简直没有了, 所以决定对其好好钻研一番, 并且写一篇对于 Qwik 的特点、根底用法、设计概念, 再加上 Qwik 对我的一些启发, 接下来就一起看看这款黑科技是何方神圣吧。

一、前提常识:ssr (懂了这里才能看懂 Qwik)

    从入门学习前端开发开始, 咱们一直学习到各种前端的优化形式来进步前端代码的性能, 其中 ” 服务端渲染(ssr)” 这种模式帮咱们大幅提高了应用前端框架开发的我的项目的首屏性能, 那么 ssr 的工作流程是什么样的? 上面咱们一起简略梳理一下。

第一步: 服务端拼接 html

   当用户申请某个页面的时候, server 端会拼接好一个页面的 html 构造返回给客户端, 例如上面的构造:

<!DOCTYPE html>
<head>
    <title>Document</title>
</head>
<body>
    <div id="App">
        <button> 点击弹出: hello</button>
        <ul>
         <li>1</li>
         <li>2</li>
        </ul>
    </div>
   <script src="/_ssr/2046328.js" defer></script>
</body>
</html>
第二步: 客户端加载好的 html 展现进去了

    下面的代码能够看出, html构造加载完就能够展现进去了, 然而比方点击事假, 这类交互事件还是没有的, 须要加载 /_ssr/2046328.js 后页面能力有交互 (活起来), 所以咱们还是要申请一堆js 文件到本地。

第三步: js 执行 hydration 阶段结束才可交互

    hydration字面意思相似 ’ 注水 ’, 也就是通过 js 代码的执行, 动静的为当前页面上的 dom 绑定事件, 你可把以后获取到的 html 代码当做一根干货海参, js代码了解成水, 而 hydration 过程就是用水把海参泡发, 达到能够食用的状态, 也就是页面可失常交互的状态。

二、ssr 流程有什么可优化的点

   看完上述 ssr 的流程后你有什么感觉? 有没有感觉 ssr 可能是个 ” 视觉骗子 ”, 咱们简略列举几个可优化的点:

  1. 尽管首屏展现的速度快了, 然而不可交互, 所以他的 tti(页面可交互工夫)并没有太大的优化, 但不可否认也是有晋升的只是不太多。
  2. 下载的 js 依然是比拟全量的 js 代码。
  3. js 代码执行的时候, 依然须要解决大量的逻辑, 还要重新处理一遍页面上的 dom。

   2020 年的时候我负责的我的项目就是应用的 ssr 技术搭建的, 首屏速度确实有进步, 然而毛病就是比拟耗费服务器资源, 并且保护老本下来了, 比方偶然的内存透露, 还有每次更新代码都须要去服务器上手动执行一些命令(过后的团队流水线还不欠缺), 给过后的我的间接感觉就是阵仗挺大收益有点小。

三、Qwik 是什么

    能够将 Qwik 了解成一款语法靠近 react 的前端 ssr 框架, 然而比传统的 ssr 框架做的更激进:

  1. 大幅优化甚至勾销了 hydration 的过程
  2. 不光是提早加载组件, 还能够提早加载点击事件等代码
  3. 简直能够做到, 只加载以后用到的 js 代码与 css 代码
  4. dom 元素没有呈现在屏幕的可视区, 则不执行组件外部办法

    Qwik的指标是提早加载所有的代码, 比方一个按钮你没有点击它之前, 那么 Qwik 就不会去加载点击相干逻辑, 甚至他都不会去加载 react 相干的代码, 毕竟有的时候用户进入页面后也的确没有进行任何操作, 那么咱们没必要去加载所有资源。

    当然了看完上述的形容你会感觉到应用 Qwik 会不会操作起来卡顿啊, 带着疑难拿好车票咱们一点点深入研究。

四、初始化我的项目

    把装置与根本用法简略说下, 这样大家脑海里就有比拟清晰的概念了, 但我感觉官网写的不错, 所以具体的用法请移步 qwik 官网

初始化我的项目

    上面这条命令就是创立我的项目的命令:

npm init qwik@latest

    第一次应用这个命令我居然愣住了, 因为我只用npm init 初始化一个空我的项目, 充其量应用npm init -y 这种形式, 然而我去查了官网才发现原来还能够这样用:

// 原命令
npm init qwik@latest

// 相当于
npx create-qwik

// 要留神, 不是
npx qwik@latest/create

所以由此可知, 咱们间接 npm install create-qwik -g 而后再create-qwik 就能够同样的初始化我的项目啦:

选项的具体能力不是本篇文章的重点, 本次咱们先从整体上体验 Qwik 当前有机会再扣扣细节。

启动

开发时的启动

npm install

npm start

打包后的启动

npm run build

npm run serve
点击事件

    因为整体上与 react 简直一样, 咱们就单刀直入, 先来看看如何定义一个组件并且定义它的点击事件:

import {component$, useStore} from "@builder.io/qwik";

export const Home = component$(() => {
  const state = useStore({count: 0,});

  return (<button onClick$={() => (state.count += 1)}>home 组件: {state.count}</button>
  );
});

    咱们能够发现, 组件是由一个 component$ 函数生成的, 有了这个函数组件就能够是一个异步的组件, 也就是当用户的屏幕上没有应用该组件时, 这个组件的相干代码就不会被加载。

    onClick$这个名字也有一个$, 意思差不多, 就是当咱们没有触发这个点击事件的时候, 不会去下载点击事件的代码, 这个就很细节了。

五、hooks

useStore 定义变量

    与 react 不一样的定义变量的写法:

const state = useStore({
    count: 0,
    name: '金毛 cc'
  });

    批改值间接在, state身上批改即可

state.name = '被批改啦'

    忽然有一种写 vue 的感觉。

useServerMount$

注册一个服务器挂载钩子,该钩子仅在首次挂载组件时在服务器中运行。

    这个 hooks 只在服务端运行, 写法如下:

  useServerMount$(async () => {console.log("什么时候执行: useServerMount$");
    const n: number = await new Promise((resolve) => {setTimeout(() => {resolve(9);
      }, 3000);
    });
    state.count = n;
  });

    打印的文字在浏览器看不到, 开发时能够在 vscode 的控制台外面查看:

useClientEffect$ 对元素的可见性的监控

    仅在客户端渲染时, 当然也有对应的生命周期办法:

  useClientEffect$(() => {console.log("初始化: useClientEffect$");
  });

    这个钩子能够监控组件是否展现在屏幕上, 也就是说只有当组件能够被用户看到时才执行, 那么咱们就来试验一下, 咱们把 home 组件顶出屏幕外, 察看 useClientEffect$ 是否执行:

    <Host>
      <h1 style={{marginBottom:'1200px'}}>Welcome to QwikCity</h1>
      <Home></Home>
    </Host>

然而当咱们滚动屏幕后展现出 home 组件:

    其实他是利用了 IntersectionObserver 这个办法监听了 dom 的状态, 所以如果咱们的某些组件外面须要展现申请到的数据, 那么咱们能够当这个组件呈现在屏幕上时再申请。

    之所以他能够提供这样的办法是因为 Qwik 框架的个性, 后续讲到 Host 组件的时候大家就明确了。

useWatch$ 订阅值的变动 (有大坑)

    上面咱们写一下, 每当 count 变动的时候, 就会触发这个watch:

  useWatch$((track) => {const count = track(store, 'count');
    store.doubleCount = 2 * count;
  });

    这里有个大坑, 就是当你的组件代码外面有 useServerMount 办法并且其在 useWatch 下方时, useWatch只能执行一次, 也就是只在 server 端执行一次, 后续不执行。

这里是谬误的用法:

// 谬误示范
// 书写在上方
  useWatch$((track) => {const count = track(state, "count");
    state.doubleCount = count + 2;
  });
// 书写在下方
  useServerMount$(async () => {const n: number = await new Promise((resolve) => {setTimeout(() => {resolve(9);
      }, 500);
    });
    state.count = n;
  });

    所以当咱们须要继续监听某个值的变动时, 须要把 useWatch 放在 useServerMount$ 上面:

// 正确写法
// 书写在上方
   useServerMount$(async () => {const n: number = await new Promise((resolve) => {setTimeout(() => {resolve(9);
      }, 500);
    });
    state.count = n;
  });
// 书写在下方
 useWatch$((track) => {const count = track(state, "count");
    state.doubleCount = count + 2;
  });

六、click 事件有大坑

    从头看完 qwik 的官网后发现, 他举的例子全部都是内连函数, 像是图里这样:

  <button onClick$={()=>state.count += 1}>
      home 组件: {state.count}
  </button>

    然而其实咱们更罕用的是上面这种模式:

 const handleClick = ()=>{state.count += 1}
 return <button onClick$={handleClick}>
      home 组件: {state.count}
  </button>

    好家伙, 我直呼好家伙, 这是不让我复用办法么? 没方法我是没有尝试胜利, 最初只好变成上面这种模式:

    这里大略得意思就是说, 这个函数不可序列化, 所以不能应用, 我又想到那只有可序列化的办法就能够放在这里么? 就有了上面的第三种写法:

    放在组件的作用域内就不行, 那么我放在组件外, 然而下方应用处仍旧报错:

    然而除非咱们将办法导出, 就不会再报错了:

    所以至多外界导入的办法依然是能够应用的, 以后组件作用域内的办法只能写在 dom 内联的办法里, 并且关键点事这些 bug 在他的官网文档里都没有具体阐明, 都是靠开发者本人去摸索, 这就让我应用体验十分差。

七、code 模版来助力

    Qwik自身组件代码有点非凡, 所以他也提供了几个代码模版帮用户生成代码, 就在 vscode 的 qwik.code-snippets 文件里:

   应用:

八、用法的改革真的好么

    从 Qwik 框架的各种用法上看得出, 他们团队的野心, 并且官网中也提到了为什么不沿用 react 的语法, 他们给出的理由是 react 以后的架构无奈做到 Qwik 想要的成果, 所以只能通过颠覆并重构的形式能力实现Qwik

    然而他们提到的所有艰难都是实现 ’ 过程 ’ 中遇到的问题, 而最终的用法应该是属于 ’ 后果 ’, 在没有 10 倍好的状况下, 开发者为什么要去学习心的写法? 并且这些写法还处处是 bug。

    当然啦, 所有的翻新都值得激励, 哪怕做出一点扭转都有可能扭转这个枯燥的世界, 然而如果用着不爽也能够慷慨说进去就是。

九、$ 缓存了什么?

    下面咱们简略介绍了下用法, 那么接下来咱们就次要聊一聊原理吧, 就从点击事件这个维度来举例, 当咱们在 button 上定义了一个点击事件, 那么编译进去的构造是这样的:

    能够看到 on:click 事件居然对应着一串 字符串, 点击事件为啥不是函数?

    其实这里是因为 Qwik 的点击事件机制, 首先 Qwik 会在全局监听点击事件, 而后当点击某个 dom 时 Qwik 会检测 dom 的身上是否有 onclick 事件并读取相应的字符串, 而后依照字符串的地址去加载对应的文件, 加载好文件后执行对应的办法。

    那咱们写的 click 事件 是如何转化成字符串的那? 这里就波及到了一个叫做 ” 的概念:

// 转换前
 <button onClick$={() => {state.count += 1;}}

// 转换后
 <button onClick$={qrl('./chunk-c.js', 'Home_onClick', [store, props])}

    所以能够了解为 qrl 办法是专门负责将一些逻辑转换到对应的须要异步加载的 js 文件 的办法, 所以这里咱们就了解了为什么组件的 onClick 事件的写法有那么多限度, 因为这些逻辑比方合乎能够独自形象到 独立的 js 文件 外面才行, 如果没法形象则无奈做到异步加载。

十、缓存是分块的: Host 标签

    Host标签是简直每个组件的最外层, 也就是如下图所示, 任何组件都要包裹一层Host:

return (
    <Host>
      ....//
    </Host>
  );

    之所以要额定包裹一层 Host 咱们来看看官网给出的解释:

宿主元素用于标记组件边界。如果没有宿主元素,Qwik 将不晓得组件在哪里开始和完结。须要此信息,以便组件能够独立且无序地出现,这是 Qwik 的一个要害个性。

    我之前写过几篇对于 react-keep-alive 的文章, 对这方面也有些理解, 在 react 内如果 dom 没有渲染到确定的地位, 那么后续再插入子组件是没有响应式的, 具体的不是一两句说的分明的, 大家能够看看我以前的文章: 一些对于 react 的 keep-alive 性能相干常识在这里

    既然官网说 ’ 必须有个 Host’ 标签, 那么就代表咱们每个组件的渲染都会多出一层 dom, 既然没法防止那就尽可能的应用它吧, 首先咱们能够指定Host 标签是什么 dom 属性:

   要留神的是, tagNamecomponent$ 办法的第二个参数:

    所以当初你这道为什么 Qwik 外面, 能够应用 useClientEffect$ 办法监控组件是否在可视区了吧, 因为组件的外层根本都有个 Host 元素, 所以哪怕是上面这种写法也能够检测元素的显隐:

<Host>
  <>
    <div>1</div>
    <div>2</div>
    <div>3</div>
  </>
    <div>4</div>
</Host>

十一、Qwik 如何解决提早? prefetch

    我最开始看这个框架就始终有一个疑难, 点击事件提早加载可能会导致微微的 卡顿 吧, 至多也是减少了点击事件的解决工夫啊, 万一这个点击事件的代码有点大并且用户的网不好, 岂不是一首凉凉?

    Qwik团队当然早已想到这些问题, 至多我是被他们给出的理由压服了, 官网的英文版本有点难懂我就用我的语言来解释一下吧:

    传统的 ssr 框架是须要全量加载 js 文件 的, 并且要等 js 文件加载结束并且 注水 结束才是页面可交互, 也就是说这些事件是 串行 的, 然而 Qwik 将其变成了并行模式, 比方 click 事件自身只对应 字符串 那么它的渲染速度当然很快, 同时 Qwik 会开启 webWorker 将代码的预取产生在主线程以外的其余线程上, 并且不是一次申请全副, 而是以后应用的几个组件的代码, 这样的话后续只有监听到点击事件申请某个文件, 则 webWorker 会将对应的文件间接传递过去, 而不必申请网络。

    并且 Qwik 还表明: 加载 js 文件执行 js 逻辑 相比, 后者可能更费时间。

    并且不是一个 js 文件 外面只有一个办法, 而是会用多个相干的办法, 比方触发了某个点击事件那么其余的一下办法也会被加载过去的, 不必逐个去加载。

十二、可恢复性

    可恢复性 Qwik推出的一个招牌概念, 咱们从三个方面聊下这个个性:

  1. 缩小 注水 : 之前也说过, 全局设置点击监听事件, 这样就不必每次加载组件都 注水 一遍能力交互。
  2. 组件树: 在传统的 ssr 模式下, 因为服务端渲染实现后可能一些 dom 构造曾经扭转, 此时就须要从新 注水 , 但Qwik 能够做到在组件代码理论不存在的状况下重建组件层次结构信, 组件代码能够放弃惰性, 我的了解这里就有 Host 组件的功绩, 比方某个 dom 的地位被调整了, 那么仅需调整 Host 即可。
  3. Qwik 容许在没有父组件代码的状况下复原任何组件, 我了解的是以后的 ssr 框架里须要父组件来创立子组件, 然而 Qwik 里将很多状态都内置了, 所以能够做到独立提早渲染, 比方 A 是父组件, a 是子组件, 那么我加载 a 组件的时候, 并不需要加载 A 组件的所有 js 逻辑。

十三、提早加载的演示, 埋点等事件

    接下来一起来看看什么时候会去加载 react 代码, 因为react 的源代码运行起来还是有点慢的, 下图展现的是一个没有任何交互时的页面申请记录:

    能够看出加载的文件十分的小, 过后第一眼看到我认为他不依赖react, 过后当咱们点击一下按钮:

    点击事件触发后才去下载core.js

    这里要留神, 实践上所有须要应用 react 的 hooks 的时候都会触发加载这个文件, 比方代码里有useClientEffect$, 那么当其执行的时候就会去下载core.js

    然而执行 useServerMount$ 这种服务端执行的 hooks 是不会触发加载 core.js 的, 所以大家晓得怎么写才更高效了吧!!

十四、我的用后感

    用起来问题还是不少的, 官网尽管也写了很多的内容, 然而具体的实例还是太少了, 并且举例不到位, 净是一些普普通通状态下的完满例子, 略微变一变就不成立了。

    并且可能是官网没怎么更新的缘故, 某些办法我粘贴过去居然不能用, 还须要我钻研良久 …

    我是不太同意推出一套与 react 差异较大的语法给到开发者, 并且单说语法扭转了的收益也并不大。

十五、我的思考

    这个框架也带给了我不少思考, 它能把那么多轻微的点做到极致, 优化入你的每一行逻辑, 那么我也有我的一些倡议:

  1. 可否让开发者随便指定哪些事件须要异步, 比方推出 onClickonClick$两个办法来辨别是否须要异步加载代码。
  2. 是否能够针对每个用户做一个点击事件触发的记录, 比方某个用户常常触发某几个事件, 那么实践上能够优先预加载这几个事件的代码, 能够通过点击事件埋点进行统计。
  3. 将常常被加载的 js 文件放在性能更好或间隔更近的 CDN 服务器上, 也就是差异化部署。

    脱离开框架自身, 对于理论业务的启发:

  1. 咱们是否能够通过埋点来关注每一个 click 事件, 比方记录每周前十的点击事件, 而后顺着这几个事件开始重点优化。
  2. 注水 也就是可交互性, 是否真的很重要, 比方某些页面是不是用户进来只是看一看就走了, 没啥须要用到 react 能力的中央, 那么这时咱们是否不必进来就加载相似 core.js 这种文件。
  3. 封装一个相似 Host 组件的组件, 能够让其内的组件呈现在可视区才加载这个组件。
  4. 一个父组件外部可能有多个子组件, 然而可能最罕用的只有一个子组件, 那么是否能够提早加载父组件与其余的子组件?

end

     这次就是这样, 心愿与你一起提高。

退出移动版