现在有一种观点声音逐渐大了起来,认为市面上出现了许多比 React 性能更好的框架,是不是意味着,React 将要被淘汰了?
所以有人就在群里问我,他觉得 Solid.js 性能比 React 更好,以后会不会取代 React?
谈谈我的看法,来做一个深入一点的分析。
先说结论:Solid.js 要取代 React 很难。
双向绑定
双向绑定的概念并非一个新的词,因此对应的解决方案 Signal ,也并非一个新的技术方案,他比 react 的存在要早得多。
Signal 是一个传统技术方案。
恰恰相反,单向数据流反而是一种技术创新。
在双向绑定的建立过程中,有一个理想的结果:我们可以轻易的知道数据与 DOM 节点的对应关系。如果这个理想的结果能够轻松达成,那么通过数据驱动 UI 的形式来开发代码将会变得非常容易。
但是真实情况是,数据与 UI 的对应关系很难建立。
双向绑定采取的措施是递归遍历监听所有数据,依次建立与对应 UI 的绑定关系。这种解决方案所花费的成本主要体现在对数据的处理上。
他面临两个问题。
一是数据的变化需要监听,但是某些数据类型的监听在实现上有难度。
以数组为例,在以前的 vue 版本中,Object.defineProperty() 因为无法监听数组长度的变化,导致 vue 不得不重写数组方法
即便如此,由于改变数组内容的方式实在有点多,要把数组设计成响应式数据反而会导致更多的性能损耗。因此 Vue 不得不提供更多的其他的方式来监听数组的变化。
比如 forceUpdate,比如大量的 Watcher,还有性能损耗更严重的 Deep Watcher。
另一个问题就是数据的层级与变化问题。
数据层级越深,我们想要深度监听,就得使用递归的方式。当数据发生变化时,部分数据与 UI 的绑定关系需要重新建立「在 vue 中,就是重复依赖收集的过程」,如果数据量过大,或者数据变化频繁,就会有性能风险。
因此 vue 官方文档也会建议大家简化数据层级,减少深度监听的成本。
基于这两个原因,导致了 vue1.x 的时候,不敢过于大声宣称自己性能更好。
为什么我要说 React 的解决方案是一种创新呢?
原因是他打破了传统的双向绑定监听数据的思路,放弃关注数据,从而绕开了上面的问题。
react 把所有的精力都放在了 UI 层。使用我们现在熟知的 diff 算法,当数据发生变化时,react 会创建一个新的虚拟DOM树,与之前的树做对比,找出需要改变的元素。
这样的好处就是完美绕开了所有的数据类型、数据复杂度、依赖收集等一系列问题,react 不仅不用头疼某些类型监听不到,也不需要担心数据量太大导致更多的性能问题。
因此在 vue2.x 的版本中,也部分借鉴了虚拟DOM的解题思路来缓解 1.x 在数据侧的压力。
从总体思路上来说,vue 的主要压力在于处理数据,react 的主要压力在于处理 UI。
react 不建立数据与 UI 的对应关系,那么也就意味着另外一个压力的产生,那就是当数据发生变化时,react 并不知道哪一个 UI 发生了变化,于此同时 react 为了保持自己对于 JavaScript 的弱侵入性,也没有在 setState 上进行任何魔改,例如绑定当前上下文从而得知具体哪个组件的 state 发生了变化。
如果进行了这个魔改,diff 的压力会小一些。
因此,每一次的 state 变化,都是整棵 DOM 树的 diff,这也成为了现在其他框架在舆论宣传上攻击 react 性能不好的重要手段和依据,也是许多人觉得 react 必将被取代的重要原因。
从解决方案来说,双向绑定方案「例如 vue,solid」的努力方向,在于如何降低数据侧的性能压力。
而 react 的努力方向,在于如何减少 UI diff 的性能压力。
Solid 的底气在哪?
后来 Vue3 宣传自己性能高于 react 的声音逐渐开始大起来了。原因就在于 Proxy 的出现。
defineProperty 在监听数据上有不少缺陷,因此基于此来实现响应式数据压力确实很大,也会给使用者在数据侧带来不小的心智负担。而 Proxy 在很大程度上解决了这个问题。
Proxy 能够监听数组的变化,能够监听删除对象字段的变化... 于是 Vue3 的底层实现,在数据侧的代码会简洁很多,并且与此同时,Vue 的后续版本,也可以彻底放弃虚拟 DOM 来进一步提高自己的运行性能。
因此,基于 Proxy 来实现双向绑定成为了许多框架的选择。这也使得许多框架有了冒头的理由和机会,Solid 的底气也来自于此。
但是,依然有一个问题没有解决。
那就是深度监听仍然需要递归。当数据量很大的时候,依赖追踪的压力也会逐渐变大。
当你的项目比较轻量的时候,你能够获得很强的性能体验。但是当你的项目变得越来越大,全局数据变得越来越复杂,层级越来越深,他的性能压力也会逐渐变大。
当然,通常情况下,我们的大多数项目也达不到这个级别。
React 项目的性能表现
React 的性能压力主要来源于 UI 侧的 diff。
当项目变得越来越大,全局状态里的数据越来越复杂。UI 侧的 diff 压力会越变越大吗?
答案是:不会。
这是一个很有意思的思考。假如我有一个超大型的项目,一共有 3000 个页面,似乎从理论的角度来说,UI diff 的压力也会增加到 3000 个页面的量级,但是事实上我们永远只会在可视区域里展示一个页面。也就是说,就算你的项目体量非常大,但是我们只会渲染一个页面。
虚拟 DOM 的 diff 压力,也只会限制在一个页面的量级,这个压力不会随着项目体量的增加而增加。这个前提,实际上就已经表明了 React 的性能不会差到哪里去。
因此在实践中,其实我们也不太需要过多的关注 react 的性能问题,哪怕是在 Fiber 架构出来之前,也不需要过多的关注。
而有意思的是,在许多文章中为了体现 solid.js 拥有巨大的性能优势,往往会构建一个实践中几乎不存在的场景,例如渲染一万个数据的列表比比谁花的时间更少。
然而事实上,即使我们不使用任何框架,就用原生 JavaScript 来渲染一万条数据,也会采用虚拟列表的方式进行优化才能确保相对流畅不卡顿。
我们一定要明白,任何框架的性能都是不可能突破原生 JavaScript 的。
react 性能瓶颈
高频率的交互往往会导致明显的性能问题。
例如表单输入时,我们期望内容的任何变化都有对应的 UI 响应,实践项目中容易出现明显的卡顿和延迟。常规的优化手段是使用防抖。
当然,在 antd 的 Form 组件也使用了将数据下放到每一个 Item 的方式来优化性能,store 中用 useRef 存储数据而不是 useState,antd 内部为每个 Form.Item 定义了 forceUpdate 来强制更新 Item UI。
又例如拖拽/resize等事件。此时我们只需要通过操作原生 DOM 的方式来实现对应的逻辑即可。从而绕开高频率的 diff 逻辑
这些性能瓶颈,大概率在 vue 和 Solid 中,也会存在。解决问题的思路也相差不大。
事实上,原生 DOM 本身在高频交互上也存在明显的性能瓶颈。因此许多前端项目不得不采用抛弃 DOM 渲染的方式来完成整个项目。但是这些项目我们仍然可以结合 react 来完成,例如著名的前端项目 Figma,或者国内有的团队使用 react + skia 的方式来完成一些对性能要求很高的项目。
一个好的思路是,不要试图用框架解决所有事情,而是让他解决他擅长的事情。
小痛点
在使用 vue 时,我们常常需要警惕对数据进行一些操作时,让数据失去响应性。在 Solid 中同样如此。
Solid 的官方文档案例中有这样一段代码。
const CountingComponent = () => {
const [count, setCount] = createSignal(0);
const interval = setInterval(
() => setCount(c => c + 1),
1000
);
onCleanup(() => clearInterval(interval));
return <div>Count value is {count()}</div>;
};
当我们使用 createSignal 定义了一个响应式数据时,此时返回的 count,并不是一个数据,而是一个获取数据的方法,注意这种差别。
在使用中,我们必须以执行该方法的形式来当成数据使用。如果 JSX 中有多处使用了该数据,我们也必须以执行该方法的形式来当成数据使用,count() 而不是 count。
如果我使用一个变量来缓存他的执行结果,然后使用该变量嵌入 JSX 中使用,该数据就会失去响应性。
var c = count()
// 失去响应性
<div>Count value is {c}</div>;
这里存在的问题就是语义与语法的错位,让我觉得不太舒服。
vue3 实际上也存在类似的问题,他为了避免这种语义与语法的错位,分别采用了 ref 来监听基础数据类型, reactive 来监听引用数据类型,虽然在 ref 的使用上任然需要借助 .value 来达到响应性,但好在实践项目中单独把基础数据类型作为响应式数据的场景非常少。
也就是说,在解决这个问题上,反而 vue3 比 solid 更加优雅,当然,即便如此,在 vue3 中,一些操作也会让数据失去响应性,例如解构,这是我们在学习的时候需要特别注意的地方。
react hooks 的痛点,闭包。
react 常常因为闭包问题,被各种攻击。认为这是 react 的缺陷。如果你没有掌握好闭包,视闭包为洪水猛兽,你多半也会认同这样的说法。
因为从表现结果上来看,闭包带来的缓存问题确实会导致使用者在理解上存在很多疑问。然而事实上,闭包问题不是 react 的问题,而是 JavaScript 本身的特性,闭包是学习 JavaScript 本应该掌握好的基础之一,只不过很多前端开发没有做到而已。
新人朋友估计在面试时,也常常被闭包相关的面试题虐哭 ~
react 提供了一个实践场景,让我们能够直面闭包带来的困扰,从而对闭包更加有掌控度,我认为这反而应该成为 react 的优势之一,而不是痛点。
但是 vue3 和 solid 都在极力的避免让开发者感受到闭包的存在,甚至把这种行为当成自己的优势来大力宣传,从我个人的角度来说,我并不赞同这样的观点,因为我们终究是要理解并掌握闭包的呀,对吧。
跨平台
Solid 为了极致的性能体验,完全弃用了虚拟 DOM,也就意味着,他放弃了跨平台的特性。只把主要精力集中在 web 项目上。也就是说,他的全局生态建设,永远也赶不上 react
到目前为止,React 已经把触手伸向了后端开发... 已经不满足简单的服务端渲染了,甚至还想要连接数据库...
这也是 Solid 无法取代 react 最重要的原因。
我们也可以自己扩展 react 的生态。比如在我的 2d 可视化课程中,我们基于 canvas 封装了一套类 DOM 的渲染引擎,然后接入 react-reconciler,就能轻松得到一个 react-echarts 的图表组件,在使用层还是 react 组件,但是在底层已经被我瞧瞧的把 DOM 换成了 canvas,或者 webGPU... ,此时,我的项目性能,将会远超 Solid.
总结
双向绑定是一种传统的解决方案,与之相对比,在前端领域 react 的解决方案是一个巨大的创新。单向数据流,Diff算法,双缓存策略,优先级队列,任务中断,浏览器空闲时间,并发,函数式编程,自定义hook... 等等许多概念都极大的扩展了前端开发的技术视野。
并不确定 react 是否借鉴了其他领域的方案,认真看过我 JavaScript 核心进阶的同学就应该知道,Fiber 架构在很大程度上借鉴了 V8 垃圾回收的底层机制。
而 Solid 作为模仿者,与 React 相比,他并没有什么突出的优势,也没有什么技术和理念上的创新。他只是满足了部分前端开发对于双向绑定 + 函数式的美好愿景而已,至于 vue 和 angular 最终都会采用 Signal 重构底层代码,那只不过是因为他们本身从一开始就是双向绑定的基因。
因此在做技术选型时,任何一个成熟的前端架构师都没有理由放弃 react 而选择 Solid,无论是从性能上考虑,还是从生态上考虑,理由都不够充分,Flutter 尚且做不到取缔 React Native,Solid 要走的路还很长。
而有的人写文章声称 Solid 比 React 还 React,Solid 教 React 写函数式,降维打击... 那只是常规的宣传用语,当不得真。