前几天,Angular
之父「Miško Hevery」和「Dan」在推上发生了一段有趣的对话,对话背景大概是:
SSR
(服务端渲染)场景下使用的技术叫Hydration
,「Miško」曾向「Dan」演示了一个新技术概念 —— Resumable
Qwik
框架中实现了Resumable
React
中我们之所以没有考虑Resumable
,并不是因为框架不好接入,而是因为Resumable
并不是更优解那么,Resumable
到底是什么技术?他和React
在推进的RSC
(React Server Component
)有什么区别?「Miško」为什么会作出上述言论?
让我们通过本文了解一下。
Resumable
的概念源于一次思路的转变。
虽然主流前端框架都支持SSR
,但不管是React
、Vue
还是Angular
,他们都是CSR
(客户端渲染)优先。
在这些框架中,SSR
是在CSR
的基础上附加的新功能。
正是由于传统前端框架都是「CSR优先」的产物,才导致一些常见SSR
问题,比如:
hydrate
完成hydrate
这些问题拉低了SSR
场景下的FCP[1](First Contentful Paint)与TTI[2]指标(time to interactive)。
下图展示了SSR
场景下hydrate
的流程,包括4个步骤,只有在整个流程完成后应用才能响应交互:
HTML
JS
文件JS
文件(主要是框架及其依赖,还有业务逻辑代码)hydrate
操作)图来自于qwik文档
在某些应用场景(比如电商、博客)下,除了第一步,其他步骤可能不是必须的。
比如,对于一个电商商品详情页,除了展示商品所需的HTML
外,其他都不是首屏渲染所必须的。
这就是Qwik
框架中Resumable
技术的设计理念 —— HTML
优先,JS
按需下载:
图来自于qwik文档
要实现Resumable
,需要抛弃传统框架以CSR
为基础(用JS
生成HTML
为主)的思路,转而以SSR
为基础(以服务端生成HTML
为主),再在此基础上附加CSR
功能。
Resumable
的理念概括起来就是「按需下载、执行JS」。
所有JS
代码的下载及运行会延迟到需要的时候再执行。在如下官方示例1[3]中,会渲染一个按钮,「按钮的点击回调对应代码」不会在首屏渲染时下载:
只有在点击按钮时,对应代码才会被下载并执行:
这就使得首屏渲染时需要下载及执行的JS
文件大大减少,提高了FCP
及TTI
指标。
实际上,如果以
Chrome lighthouse
的评分作为评判依据,其他框架确实都难以望Qwik
的项背
这项技术之所以叫Resumable
(恢复),是因为它与传统Hydration
技术在首屏渲染时客户端逻辑的区别。
传统Hydration
技术在首屏渲染时,客户端(比如浏览器)会全量执行框架代码与业务逻辑代码,并在此过程中完成:
React
中叫Fiber
树,在Vue
中叫VNode
树)而以上过程在Resumable
技术中是发生在服务端的。比如,对于上述按钮的例子,点击回调对应的下述代码会在服务端生成HTML
时完成序列化:
序列化后的数据会以HTML属性
的形式存在:
当点击事件发生后,框架的前端部分会根据HTML属性
(示例中的on:click
属性)向后端请求具体的JS
代码(即点击回调对应的代码)并执行。
一句话总结就是 —— 在Resumable
技术中,一切以SSR
为主,部分在SSR
时未完成的操作(比如交互逻辑对应代码)会在需要触发时(比如交互发生时)再「恢复」执行,所以这一技术叫Resumable
(恢复)。
同样是SSR
相关技术,React
团队主导的RSC
(React Server Component
)与Resumable
有什么区别呢?
在讲解他们的区别前,我们要先了解一个背景知识:React
是「CSR优先」的框架,而且他已经出现很多年了(13年问世)。
虽然这些年出现了很多优秀的框架技术(比如Signal
、AOT
),但React
一直坚持这套「重客户端运行时」技术架构。
在发布React Hooks
后,React
团队逐渐将重心转移向服务端。由于其技术架构偏向客户端运行时,所以将React
直接改造为「SSR优先」显然不现实。
为此,React
团队的策略是 —— 提供SSR
能力,再让其他「SSR优先」框架接入(主要是Next.js
)。
所以,Resumable
与RSC
的主要区别其实体现在框架底层实现层面。
最大的区别体现在「序列化数据」方式的不同。
在Resumable
技术下,SSR
时会将大量数据序列化为HTML
属性或注释,比如:
DOM
与Qwik
组件的关系HTML
属性,再在客户端恢复)服务端完成了大部分工作,客户端需要做的仅仅是按需反序列化数据,并执行对应逻辑。
在RSC
中,服务端组件会被序列化为一种自定义JSX
协议,并被流式传输。之所以没有被序列化为HTML
字符串(就像Resumable
那样),是因为数据被反序列化后并不直接是HTML
,而是JSX
,JSX
经由React
处理后才会映射到HTML
,这么做能保持服务端组件的子孙客户端组件不丢失状态。
比如如下RSC
,根据id props
从数据库取不同数据,再将数据传递给子组件(客户端组件):
当id props
变化后,ClicentCpn
组件内的状态并不会丢失。就是因为服务端传输来的ServerCpn
是一种自定义JSX
协议,而不是HTML
字符串。
通过区别1可以发现,RSC
中序列化的数据描述的是组件级别的内容(JSX
描述组件)。
而Resumable
中序列化的数据粒度更细(比如描述点击事件的回调逻辑,或者某个状态)。之所以会有这种区别,是因为两个框架采用不同的变化监测方式。
当状态变化后,React
需要遍历完整的组件树才能计算出「状态变化产生的影响」。所以序列化数据只需要描述组件级别的内容就行。
而Qwik
(实现Resumable
技术的框架)使用Signal
监听状态变化,这使得他能精确定位「状态变化所产生的影响」,即精确定位状态变化需要反序列化哪些数据。
由于React
是重客户端运行时的框架,所以虽然RSC
是SSR
技术,他的后续发展还是会与重客户端运行时的技术绑定(比如Suspense
、Selective Hydration
)。
Resumable
是重服务端技术,所以后续发展应该会围绕服务端展开,比如:
class
序列化)了解了这些技术细节,让我们回到开篇,为什么「Miško」会怼React
呢?
实际上,这并不是「Miško」第一次对React
发表看法。之前「Miško」就曾表示:即使React Forget Compiler
成功问世,他也没法解决props下钻
场景下的性能问题,并以此论证Signal
技术的优越性:
在这里我们不比较技术优劣。只是说单纯用脚投票,除了React
外,确实有很多框架都使用了Signal
相关技术,比如:
Vue
Preact
Qwik
Angular
Solid.js
在「Miško」看来,React
团队之所以不采用更优秀的技术,是由于一旦采用新技术,就没法完美的向后兼容,势必造成社区生态的割裂。
作为Angular
的作者,「Miško」对这种后果再清楚不过了。
但是,React
团队却认为 —— React
之所以没有采用这些技术,是因为自身的技术路线更优秀。
这里「Dan」举出的例子是Hooks
和RSC
。
本文已经做过RSC
与Resumable
的比较。在笔者看来,两者是不同技术路线(CSR
优先还是SSR
优先)下的优秀代表。
但就Hooks
而言,笔者认为Hooks
优秀在其理念,而不是实现。同样基于Hooks
理念实现的Vue Composition API
在使用体验上比React Hooks
更佳,比如:
之所以同样理念的不同实现使用体验不同,完全是由于底层的技术实现区别造成的(这里指「底层变化监测方式」)。
所以,从这个角度想,笔者并不赞同React
团队的说法。
我想,这也是为什么「Miško」会认为React
团队吃不到葡萄说葡萄酸。
大佬们的讨论总是理性、互相尊重且克制的。「Miško」在后续也表示了自己对React
的误判。
在Qwik v1.0
发布时,「Dan」第一时间送上祝福。
有意思的是,对于「Dan」的祝福,「Miško」回复道:我们都站在巨人(指React
)的肩膀上。
这是不是说,我还是比巨人要高呢?
[1]
FCP: https://web.dev/fcp/
[2]
TTI: https://developer.chrome.com/docs/lighthouse/performance/interactive/
[3]
官方示例1: https://qwik.builder.io/examples/introduction/runtime-less/