性能爆表的SolidJS

11,431 阅读9分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

使用预编译、无虚拟DOM、究极融合怪、性能爆表、React的异父异母亲兄弟——SolidJs

背景

前段时间,产品提了个临时需求,让我给开发一个独立部署的首页可视化页面,因为目前我们系统提供以iframe的方式实现用户自己选择URL配置一个页面,作为应用的其中一个路由页面使用~

想着就单独部署一个页面,也没必要使用React或者Vue了,简单画个页面完事,jq我是用不动了

想着最近出社区也涌现了不少有趣的框架,之前看svelte,就觉得挺有意思的,感觉也比较符合我的使用场景,正准备用这个上手搞一波呢

然后,在GitHub发现了solidjs这个项目,大致看了下,好家伙,这简直比reactreact😆~

看了下文档,写了demo试了下,很容易上手,又看了一些对比测试和博客介绍,感觉性能很强啊,和svelte一样都是预编译,没有运行时,构建产物十几kb,与原生js相差无几,令人惊叹~

对于我这种小项目还是比较适合的~

不多逼逼,直接上手干🧐🧐~

介绍

官方介绍:用于构建用户界面的声明式、高效且灵活的 JavaScript

Solid 使用了类似 Svelte 的预编译,语法使用上类似于 React,使用 JSX 语法和非常相像的API,但不同于 React,组件只会初始化一次,并不是 state 改变就重新运行渲染整个组件,这类似于 Vue3setup和响应式更新(更新颗粒度为节点级)

官方给出的理由:

  • 高性能 - 始终在公认的 UI 速度和内存利用率基准测试中名列前茅
  • 强大 - 可组合的反应式原语与 JSX 的灵活性相结合
  • 务实 - 合理且量身定制的 API 使开发变得有趣而简单
  • 生产力 - 人体工程学和熟悉程度使构建简单或复杂的东西变得轻而易举

主要优势

  • 高性能 - 接近原生的性能,在 js-framework-benchmark 排名中名列前茅
  • 极小的打包体积 - 编译为直接的DOM操作,无虚拟DOM,极小的运行时(类似于 Svelte),适合打为独立的 webComponent 在其它应用中嵌入
  • 易于使用 - 近似 React 的使用体验,便于快速上手

对比分析

我们把关注点聚焦于是否使用虚拟DOM,以及数据的响应处理。

虚拟DOM的分析

首先,虚拟DOM并不是一定比原生性能好,或者说是更快,抛开真实场景不谈都是瞎扯淡,框架的设计和应用场景是有它自身考量的。

在状态与Dom操作之间抽象出一层虚拟Dom,需要牺牲一定的运行时性能,并不一定比直接操作原生Dom快,要看情况,毕竟diff并不是免费的。

  1. 不管你的数据变化多少,每次重绘的性能都是可以接受(提供过的去的性能)。
  2. 你依然可以用类似 innerHTML 的思路去写你的应用。
  3. 最最重要的一点,实现了跨平台。

react,对于web端的渲染可以使用react-dom,对于native的渲染可以使用react-native、以及服务端渲染等,他们的开发模式非常类似,按照react的语法规则进行即可,但是在render层,只要符合react api规范,你可以提供各种不同的render渲染函数,进行跨平台的渲染实现。

核心原理的选择

拿我们熟悉的reactvue说明:

  • React对数据的处理是不可变(immutable):具体表现是整树更新,更新时,不关注是具体哪个状态变化了,只要有状态改变,直接整树diff找出差异进行对应更新。
  • Vue对数据的处理是响应式、可变的(mutable):更新时,能够精确知道是哪些状态发生了改变,能够实现精确到节点级别的更新(类似的框架还有Svelte、SolidJS)。

更新粒度的选择

  • 应用级:有状态改变,就更新整个应用,生成新的虚拟Dom树,与旧树进行Diff(代表作:React,当然了,现在它的虚拟Dom已升级为了Fiber)。
  • 组件级:与上方类似,只不过粒度小了一个等级(代表作:vuev2及之后的版本)。
  • 节点级:状态更新直接与具体的更新节点的操作绑定(代表作vue1.xSvelteSolidJS)。

vue1.x时代,对于数据是每个生成一个对应的Wather,更新颗粒度为节点级别,但这样创建大量的Wather会造成极大的性能开销,因此在vue2.x时代,通过引入虚拟DOM优化响应,做到了组件级颗粒度的更新。

而对于react来说,虚拟DOM就是至关重要的部分,甚至是核心,我们已经了解react是属于应用级别的更新,因此整个DOM树的更新开销是极大的,所以这里对于虚拟DOM+diff算法的使用就是极其必要的。包括现在的fiber架构与可中断更新,也算是对虚拟DOM的极致压榨。

是否采用虚拟DOM

这个选择是与上边采用何种粒度的更新设计紧密相关的:

  • :对应用级的这种更新粒度,虚拟Dom简直是必需品,因为在diff前它并不能得到此次更新的具体节点信息,必须要通过随后的虚拟Dom+Diff算法筛选出最小差异,不然整树append对性能是灾难(代表框架:Reactvue)。
    • 但这里值得注意的事是:本质上vue并不需要虚拟DOM,因为它这种基于依赖收集的响应式机制可以直接进行节点级更新,但vue借助虚拟DOM的抽象能力,可以做到更新粒度的随意调整(目前是组件级),给vue的发展提供更多可能性, 尤其在跨平台渲染方面,这点十分关键。
  • :对节点级更新粒度的框架来说,一般没有必要采用虚拟dom(代表作:vue1.xSvelteSolidJS)。

开发语法DSL选择

  • JSXReactSolidJS
  • 模版+编译指令vue(JSX可选)、Svelte

正文

基本使用

import { render } from 'solid-js/web';
import { createSignal, createEffect } from 'solid-js';

const CountingComponent = () => {
  const [count, setCount] = createSignal(0);

  createEffect(() => console.log('count', count()));

  const handleAdd = () => {
    setCount((prev) => prev + 1);
  };

  return <div onClick={handleAdd}>Count value is {count()}</div>;
};

render(() => <CountingComponent />, document.getElementById('app'));

这简直是React hooks的双胞胎兄弟...

而且因为SolidJS这种后发优势,没有React沉重的历史包袱,比如不需要处理类组件的兼容(SolidJS只支持函数式)这让它在实现了大部分React功能特性的前提下,源码体积要比React小很多,这让它在首屏加载方面就首先占据上风。直接调用编译好的DOM操作方法,省去了虚拟DOM比较这一步所消耗的时间,整个更新链路相比React变得简洁许多。

调用栈分析:

solidjs-1

solidjs-2

简单分析:

  1. 组件函数只会在整个应用生命周期里调用一次。
  2. 心智模型与react完全不一样,反而与vue3保持了一致,可以说兼具了React hooks + vue3的优点
  3. createEffect自动追踪依赖,不需要像react那样维护一个dep数组
  4. hook调用顺序没要求,以函数调用的方式解决Proxy目标必须是对象的问题

它的响应式实现确实是与vue一样,都是基于发布订阅的依赖收集去做的,但它没有采用vue虚拟Dom的运行时diff,而是充分在编译阶段做文章,将状态更新编译为独立的DOM操作方法。

编译内容分析

import { render, createComponent, delegateEvents, insert, template } from 'solid-js/web';
import { createSignal, createEffect } from 'solid-js';

const _tmpl$ = /*#__PURE__*/template(`<div>Count value is </div>`, 2);

const CountingComponent = () => {
  const [count, setCount] = createSignal(0);
  createEffect(() => console.log('count', count()));

  const handleAdd = () => {
    setCount(prev => prev + 1);
  };

  return (() => {
    const _el$ = _tmpl$.cloneNode(true);
          _el$.firstChild;

    _el$.$$click = handleAdd;

    insert(_el$, count, null);

    return _el$;
  })();
};

render(() => createComponent(CountingComponent, {}), document.getElementById('app'));

delegateEvents(["click"]);

可以看到,跟基于 Virtual DOM 的框架相比,这样的输出不需要 Virtual DOMdiff/patch 操作,自然可以省去大量的运行时代码。而是使用了solid-js/web库提供的insert等DOM函数操作。

再结合以后的webcomponent考虑下,真是大有可为,发展空间很大,未来可期~

项目实战

直接按照官方文档示例,创建一个支持TypeScript的基础项目,模板默认使用vite构建(solidjs-templates

# Typescript template
$ npx degit solidjs/templates/ts my-solid-project
$ cd my-solid-project
$ npm install # or pnpm install or yarn install

在vite中引入插件

import solidPlugin from "vite-plugin-solid"

export default defineConfig({
  plugins: [solidPlugin()],
})

入口文件配置如下:

import { render } from "solid-js/web"
import App from "./App"

render(() => <App />, document.getElementById("root"))

接下来就可以开始写业务代码了,就是这么简单~

import { Title, List, Chart } from "./components"
import { onMount, onCleanup, createSignal } from "solid-js"
import request from "./utils/request"
import type { Component } from "solid-js"
import type { DataProps, ValueType } from "./typings"
import cls from "./index.module.less"

const URL = "/statistic/hrm"

const App: Component = () => {
  const [getValue, setValue] = createSignal<ValueType>(null)

  // mount
  onMount(() => {
    request<DataProps>({ method: "GET", url: URL }).then((res) => {
      if (res) {
        console.log("res", res)
        setValue(res)
      }
    })
  })

  // unmount
  onCleanup(() => {
    // ...
  })

  return (
    <div class={cls.App}>
      <Title />
      <List list={getValue()?.list} />
      {getValue()?.pieData && <Chart data={getValue()?.pieData} />}
    </div>
  )
}

export default App

List组件

import type { Component } from "solid-js"
import { ListProps } from "../typings"

import cls from "../index.module.less"

// List
const List: Component<{ list: ListProps[] }> = (props) => {
  return (
    <ul class={cls.list}>
      {props.list?.map(({ label, value }) => (
        <li>
          <div class={cls.label}>{label}</div>
          <div class={cls.value}>{value}</div>
        </li>
      ))}
    </ul>
  )
}

export default List

Echart可视化组件

import { onMount, onCleanup } from "solid-js"
import type { Component } from "solid-js"
import echarts, { ECOptionPie } from "../../utils/echart"
import { OptionProps } from "../../typings"

// Chart
const Chart: Component<{ data: OptionProps[] }> = (props) => {
  let container: null | HTMLDivElement = null
  let instance

  // 性别分布
  const Option: ECOptionPie = {
    // data: props.data || [],
    // ...
  }

  onMount(() => {
    instance = echarts.init(container)
    instance.setOption(Option)
    window.addEventListener("resize", () => instance?.resize())
  })

  onCleanup(() => {
    window.removeEventListener("resize", () => instance?.resize())
  })

  return <div ref={container}></div>
}

export default Chart

以上是我基于项目简化的demo,怎么样,看起来是不是和react特别像🤓,使用起来也是相当简单了~

总结

自react和虚拟DOM诞生以来,整个前端的开发范式都发生了翻天覆地的变化,各种类似框架也是层出不穷,他们各有各的优势。

对我们开发者来说,对于同一类型框架熟练掌握一种足矣,大可不必每种框架都学习一遍,我们需要做到对其内部实现原理的知悉,做到知其然也知其所以然,正所谓一法通万法皆通,当我们打牢基础之后再去使用和学习其他框架便轻而易举了,并在实践中拓展知识广度和深度。

对于不同类型框架,了解其优势以及一些独有的特殊思路和实现,做到心中有数,也有益于我们的技术成长。

这样我们在之后的实际开发过程中便可结合具体场景做到更合适的技术选型~

参考