比React还Vue3的框架SolidJS,为什么性能可以Number 1?
前言
SolidJS 是什么呢,这位小朋友可谓是非常的“谦虚”,号称支持现代前端特性:JSX、Fragments、Context、Portals、Suspense、Streaming SSR、Error Boundaries、并发渲染等现代功能。无论是客户端还是服务端,目前的性能评分都最高!
咋一看好家伙,这是什么都有啊,让其他框架怎么活,但当你真的了解它后才会发现这真的是一个宝藏框架呐~
另外由于是后起之星,也没有那么重的历史包袱(类比 React Hook 原理需要兼容 class Component 导致源码过于复杂),其代码非常易读,可以作为新手入门学习使用。
任何框架都可以看作是思想与设计模式的组合,所以我们可以抱着学习的心态去看待这款框架。
1. 初试牛刀
首先我们看一个简单的计数器 Demo,
import { render } from 'solid-js/web';
import { createSignal, createEffect } from 'solid-js';
const Counter = () => {
const [getCount, setCount] = createSignal(0);
const add = () => setCount(getCount() + 1);
createEffect(() => {
console.log('count is change:', getCount());
});
return (
<button type='button' onClick={add}>
{getCount()}
</button>
);
};
render(() => <Counter />, document.getElementById('root'));
咋一看,嚯~ 这不就是 React 嘛~
还有 effect
也不用手动声明依赖了,看着又有一点 Vue3 的影子。
来看看上面代码的打包产物,
发现js文件竟然不到8K
,真的可以这么精致吗!
其实呀,SolidJS不仅打包体积小,性能也是 Number 1!
来看看 js-framework-benchmark 跑分结果
有同学说了,你这不乱说嘛,第一明明是 Vanilla !
实际上,Vanilla 就是纯粹的原生 JavaScript,通常作为一个性能比较的基准。
那么 SolidJS 为什么既做到了体积小,还做到了性能强,甚至超越了前端巨头 Vue、React 呢?
接下来我们简单分析其原理来一探究竟!
2. 庐山真面目
2-1、平衡了 jsx 与 template 的利弊
说到这里不得不提一下 jsx 和 template 的优缺点
- jsx
- 优点:作为
js
的语法糖拥有高度灵活性,可以随意编写 - 缺点:因为过于灵活在 编译阶段 很难分析操作意图
- 优点:作为
- template
- 优点:因为语法有限制,大部分带有 操作意图(v-if、v-for) 的代码都可以在 编译阶段被识别以做优化
- 缺点:写法受限,大部分情况下不如
jsx
灵活
有同学可能会疑惑了,编译阶段 能做什么,识别操作意图 有那么重要吗?
说到这里,不得不提一下 Vue3,
Vue3 对比 Vue2 性能之所以实现了一个质的飞跃,这其中就离不开 编译阶段优化。
-
1、 比如在编译阶段标记出
template
中永远不会变化的节点作为静态节点存储,将来更新时直接绕过他们; -
2、提前对
v-if、v-for
这一类区块做区分,将来diff时绕过不必要的判断; -
3、绑定
props
时记录哪些属性可能会变,将来 diff 时只对比“可能会变化的动态节点和属性”,跳过“永远不会变化的节点和属性”。 -
除此之外还有缓存事件处理程序等等
这么一看,编译阶段是很有必要哈,但jsx
为什么不能做这些呢?
想象一下,假如我们要做一个 根据条件渲染组件 的功能,
在 template 中默认我们只能用指令v-if
来实现:
<template>
<div>
<span v-if="status === 1">通过</span>
<span v-else-if="status === 2">拒绝</span>
</div>
</template>
但在 JSX 中有很多写法,随便就可以想到三种:
return status === 1 ? <span>通过</span> : status === 2 ? <span>拒绝</span> : null;
return (
<>
{status === 1 && <span>通过</span>}
{status === 2 && <span>拒绝</span>}
</>
);
switch (status) {
case 1:
return <span>通过</span>;
case 2:
return <span>拒绝</span>;
}
如果每种情况都去判断一遍,那么 编译阶段 将会非常复杂且耗时,另外显得也非常麻瓜。
Vue3 之所以做了那么多 编译时 优化离不开其特定的 模板语法 ,在限制约束的前提下做了很多可识别的优化。
而 JSX
因为太过于自由使得很难做 意图分析,看来太自由也不是一件十全十美的事情哈
SolidJS 采用的方案是:在 JSX 的基础上做了一层规范,中文译名为 控制流
写法上类似某种预设的组件,用于编译阶段优化:
return (
<Switch>
<Match when={status === 1}>
<span>通过</span>
</Match>
<Match when={status === 2}>
<span>拒绝</span>
</Match>
</Switch>
);
这样在编译阶段就可以做意图分析,提前知道这是在做按条件渲染,然后编译成对应的dom操作即可。
可以简洁概括为:
-
即借鉴了 template 更容易做编译阶段优化的优势
-
又保留了 JSX 的灵活性
2-2. No Dom Diff
No Dom Diff 是说 SolidJS在更新粒度方面,摒弃了虚拟dom,采用节点级更新。
说到更新粒度,可以先总结下目前前端主流的几种方案:
- 应用级更新:状态更新会引起整个应用
render
,具体渲染哪些内容取决于协调的结果。代表作有 React - 组件级更新:状态更新时只会引起绑定了该状态的组件渲染,具体渲染哪些内容同样取决于协调的结果。代表作有vue2.x
- 节点级更新:状态更新时直接触法绑定该状态的节点更新,也就是指向型更新。代表作有vue1.x、Svelte、SolidJS
或许你会奇怪了,React 不也是组件级更新吗?
那我们可以思考一个问题:为什么hook
有顺序的限制、useEffect
中有脏数据?
这是因为 React 每次更新都会重新走一遍更新流程,做这些限制是为了获取到完整的VDom树/Fiber树,通过 diff新旧两棵树来决定真正更新哪些组件,所以 React 并不是组件级更新。这也就是为什么在React Fiber架构出现之前如果优化不当容易出现更新卡顿问题,而Vue2.x并不会的原因。
Vue1.x当初摒弃了节点级更新,是因为那时候的 Vue响应式原理 采用三大对象:Observer、Dep、Watcher,三者都是复杂对象(参考复杂对象和简单对象),由于还要递归观察data
下的子对象,所以一个应用中的观察者会非常多,很容易因占用过多内存而出现卡顿问题。所以到了Vue2.x就改用组件级更新了。
而SolidJS对于三大对象均采用简单对象存储,另外不需要递归观察,所以占用内存非常少。
解决了占用内存多的问题,接下来是如何更新dom,具体的做法是:在编译阶段提前生成类似 insert
、update
、delete
的dom操作方法,将来更新时直接调用。
或许有的同学还有其他疑惑:虚拟 dom 当初诞生的原因可不仅仅是为了节省 dom 开销,还有一个更重要的原因是为了跨平台开发(React Native、Weex),那 SolidJS 如何实现跨平台渲染呢?
细心的你可能已经发现了这行代码
import { render } from 'solid-js/web';
没错,SolidJS 和 React 的理念非常像,作为 渐进式框架,他们都将 核心库 与 渲染库 分离开来,
solid-js/web 是 web 平台 的 节点操作方法集,如果我们将来要做 自定义渲染,只需要再实现一个 solid-js/Android 或者 solid-js/IOS 即可,是不是思路很新颖,跨平台与性能兼得!
2-3. 重·编译时
-
提前生成节点渲染方法
刚才说到 SolidJS 在 jsx 中借鉴了部分 template 的规范写法,在编译阶段 分析意图,提前生成对应的dom操作方法
-
按需打包,缩小体积
这一步也就是 tree-shaking,只打包用到的模块,近一步缩小打包资源体积。
2-4. 轻·运行时
由于没有了diff这一大规模计算,使得运行时代码更轻量,所以SolidJS在更新时也更简洁。
-
这是 SolidJS 在更新时的js调用栈
-
而这是 React v16 在更新时的js调用栈
另外这一些特性使得另外一件事情更容易落地:微前端
现在微前端的一大痛点是多个项目打包出来的资源体积过大,而轻运行时会使得代码体积将对小巧,也因此非常适合做Web Component(这是w3c提出的一个新标准)。想象一下:未来Vue、React都做到了轻·运行时,我们只需要把组件编译成Web Component,然后在主应用直接引用即可。再也不用为不同技术栈、不同版本差异所苦恼了!
2-5. 不被顺序限制的 hook
说到前端框架中的 Hook,最先将这个方案落地的是React,但由于React一直推崇 immutable 思想,每次更新必须重新走一遍整个树的更新流程,使得 React Hook 不可以在条件循环中使用,否则可能使渲染结果受到影响,注意是可能,不是一定哦~
后来尤大发布了Vue3.0,伴随而来的一大特性是Composition API,俗称Vue3 hook,由于Vue2以后都采用组件级的更新粒度,再加上响应式原理采用的是自动收集依赖,所以Vue3 hook不会有顺序/条件的限制,另外还可以嵌套使用。
SolidJS的响应式原理主要借鉴了React Hook的思想,同时也保留了Vue3的依赖收集模型,所以用起来非常丝滑。
不过有意思的是,对于 number
、string
、boolean
这一类非引用类型 state
,Vue3 将其自动封装成具有 current
属性的对象:
const count = ref(0)
const show = () => {
console.log('count:' + count.value)
}
因为基本数据类型不可以被Proxy
代理,无法实现数据劫持;
到了SolidJS,干脆就把获取数据的方式变成了 getter 方法:
const [getCount, setCount] = createSignal(0)
const show = () => {
console.log('count:' + getCount())
}
在方法内部同样可以做 数据劫持 ,简直巧妙呀!
大概流程是这样:
- 在
getter
方法内部将依赖收集起来 - 在
setter
方法中触法依赖更新
与 Vue3 不同的是,Vue3 依然保留了 虚拟 dom ,所以 diff 依然会占用 运行时 时间,
SolidJS则忽略了这一步,所以性能更好。
3. 其他
-
脚手架:degit,内部集成了 vite。
-
支持
TS
且类型友好 -
现代前端框架大部分特性:
Fragments
、Portals
、Context
、Suspense
、事件委托
、SSR
等等
真是典型的“我全都要”
4. 总结与思考
大家都说前端的技术发展是一个轮回,
比如目前比较火热的 Tailwind css,很像现代版的 bootstrap
SSR 很像当初 jsp 的现代版
前端框架的打包产物逐渐缩小体积,试图回到原生js项目的体积
而 原生 DOM 经历了 虚拟 DOM 会不会又会慢慢回归到 原生 DOM呢~
当然最关心的一个问题就是 SolidJS 能否上 生产环境 ?个人感觉还是先不要着急,可以用在 个人项目 练练手,或者作为一个学习源码的好机会(毕竟这个框架拥有现代前端的大多数特性,而且没有那么重的历史版本包袱,相比React还是比较容易读的)
转载自:https://juejin.cn/post/7018828149567782926