大家好,我是Kason。在最近的WWC22上,builder.io的CTO“mi?kohevery”(也是Angular/AngularJS的发明者)发表了一段富有想象力的演讲。mi?kohevery在演讲中介绍了一个全栈SSR框架——Qwik,号称“可以帮你去掉项目中99%的JS代码”。他是怎么做到的,本文我们将介绍Qwik。表现不佳?如果你不想做coder,那我们先说说Qwik诞生的背景。对于很多2C的Web应用(比如电商)来说,首屏性能指标与用户留存相关,而用户留存与赚多少钱有关。因此,应用程序打开的速度会影响您赚多少钱。然而,对于前端开发者来说,首屏性能指标并不容易优化。原因并不是开发人员不够努力。让我们看一下两个性能指标。如何优化FCPFCP(FirstContentfulPaint)测量“从页面开始加载到页面内容的任何部分呈现在屏幕上的时间”。目前Web应用普遍使用“前端框架”开发,这意味着从HTML解析到最终页面会引入大量的JS代码(框架本身的代码,第三方依赖包的代码……)渲染。:下载框架JS代码。执行框架JS代码。页面渲染由框架完成。这导致FCP指标下降。为了优化FCP,框架的作者提出了SSR(ServerSideRender,服务器端渲染),在服务器端生成首屏需要的HTML,为FCP节省了上述三个步骤所需要的时间.但是,TTI指标仍需优化。如何优化TTITTI(TimetoInteractive,用户交互时间)来衡量“页面变成完全交互所需要的时间”。主要衡量的是从以下1到3所需要的时间:首先衡量FCP时间。将事件绑定到页面上的元素。与元素交互后,事件响应时间在50ms以内。使用SSR后,虽然FCP降低了,但是框架水化(注水,即框架让页面响应交互)所需的时间会对TTI产生影响。可见性能瓶颈的根源在JS代码。React18的SelectiveHydration通过“优先补水与用户交互的部分”来优化TTI指标。然而,Qwik更为极端。他的目标是杀死所有不必要的JS耗时。这里的耗时包括两部分:加载JS为静态资源的耗时。耗时的JS运行时。超超细粒度水合物如果说传统SSR的粒度是“整页”。那么React18的SelectiveHydration的粒度就是“交互组件”。那么Qwik的粒度就是“组件中的某个方法”。例如下面是HelloWorld组件(可以发现Qwik使用了类似React的语法):对应的页面渲染效果:打开浏览器网络面板,这个页面会有多少JS请求?由于这是一个静态组件,没有逻辑,所以答案是:没有JS请求。让我们来看看经典的Counter组件。与HelloWorld相比,增加了“点击按钮状态变化的逻辑”。代码如下:对应的页面渲染效果:打开浏览器网络面板,这个页面会有多少个JS请求?答案又是:没有JS请求。注意,在这两个组件的代码中,component$是用来定义组件的,并且有一个$符号。在Counter中,onClick$回调也有一个$符号。在Qwik中,以$为后缀的函数是“延迟加载”的。hydrate的粒度有多细取决于$的定义有多细。比如Counter中,如果onClick$有$后缀,那么点击回调就是懒加载的,那么首屏渲染就不会包含“点击后的逻辑”对应的JS代码。点击按钮后,会发起两个JS请求。第一个请求返回“点击后的逻辑”:第二个JS请求返回“组件重新渲染的逻辑”:这两段代码执行完后,Counter变为1。如果查看元素,会发现在点击之前,“逻辑地址”保存在buttonon:click属性中:点击后,会从对应地址下载JS代码,执行对应逻辑。从优秀到极致,你觉得优化到极致了吗?还没有。对于一些长期存在于页面中,需要JS驱动的模块(比如轮播),在模块展示之前,不需要“JS对应的模块”。比如下面这个时钟的例子,页面上有一个长长的列表,比一屏高,列表的底部有一个时钟。下面是列表滚动到底部时的样子:在Clock组件的useClientEffect$中定义“时钟指针摆动逻辑”:Qwik中也有一个类似于React的useEffect,但是在Qwik中这个Hook可以在服务器/客户端上执行。为了区分,useClientEffect是“只在客户端执行的useEffect”。加上$后缀表示是“懒加载”。具体效果是:当页面滚动到铃铛响起时,不会请求useClientEffect$对应的JS代码。暴露时钟后,会发起两次JS资源请求:useClientEffect的逻辑。重新渲染Clock组件的逻辑。如果勾选元素,在时钟暴露之前,指针对应的元素不会移动:当时钟暴露时,会加载执行JS代码,然后开始动画:fordatahydrateIntraditionalSSR,数据实际上被初始化了两次:第一次渲染页面,服务端导出的HTML已经携带了首屏渲染的数据。帧水合后,将数据转换为帧中的状态,用于后续渲染。在Qwik中,页面初始化的时候,会有一个qwik/json类型的脚本标签,用来存放“当前页面激活状态对应的数据”:什么是“激活”?比如下面是一篇文章的评论区。这是首屏渲染后的样子:这些评论数据会出现在qwik/json保存的数据中吗?不,因为没有交互来激活它们。我们发现一个评论被折叠了,点击后评论会展开:点击这个行为会请求:点击逻辑对应的JS代码。该注释对应于组件的重新渲染逻辑。这时候评论数据只会出现在qwik/json中,因为点击交互激活了这个数据。所以在Qwik中,如果没有必要,数据不会被初始化两次。HTML中有“inactivedata”,“activateddata”保存在qwik/json的script标签中。这个功能会带来一个很有意思的效果:在调试工具中复制“Elements面板下的DOM结构”后,再粘贴到新页面中,重现“页面当前的交互状态”(比如输入框仍然保留之前输入的内容):将红框中的内容复制到另一个框架中,只能重现“当时的页面初始状态”。交互的时候请求JS会不会卡?可能有同学会问,如果网络不好,在交互的时候请求JS代码不会导致交互卡顿吗?Qwik允许你指定“哪些组件很可能被用户高概率操作”(例如,在电子商务应用中,购物车按钮被点击的概率很高)。这些组件逻辑对应的JS代码会被预取,在不影响首屏渲染的情况下被预请求:并且这些组件预取的顺序是可以调整的。这意味着可以跟踪用户行为,并以“用户交互频率”为指标,作为组件预取优先级的依据,启发式地提升应用性能。这才是真正的“面向用户”的性能优化,而且是全自动的。总结一下,今天是一个前端框架百花齐放的时代,不同的框架都在寻找自己独特的卖点。Qwik的卖点是将JS代码的拆分从常见的“编译时”(如webpackchunking)、“运行时”(如动态导入)改为“交互时”。JS代码的最终拆分只是为了达到一个目的——在首屏渲染时去掉你项目中99%的JS代码。对于这波操作,你怎么看?
