Svelte——不止于快

Svelte小结

最近组里准备对我们框架中基于 VDOM 来更新 UI 的这一套架构有所不满,所以此番我调研了一番近期比较火的Svelte,想看看如果不用 VDOM ,能否搞出一番新天地。

背景 && 痛点

首先讲下 Svelte 框架出现的背景,现在的前端框架日益膨胀,有的时候仅仅为了显示一个简单的页面,都要加载几百KB的资料,以github上很火的 RealWorld 项目为例,可以看到用 Svelte 来开发并生成的 bundleSize 非常小,而其他框架最后得到的bundleSize则很大,这是因为很多前端框架都会有 runtime 这一个概念

runtime指的是框架本身的代码也会被打包进用户的代码里,生成一个bundle.js供用户下载,用户的各种操作都会执行runtime中的一些辅助函数(以React为例,这些辅助函数包括了state的计算、state的合并、diff算法的执行……)

Svelte——不止于快_第1张图片

Svelte 是不依赖于 VDOM 的,这与当下流行的一些前端框架(React、Vue)有很大的不同,那么为什么 Svelte 要摒弃 VDOM,朝另一个方向上走呢?下面就要来讲讲 VDOM 的优缺点

Virtual DOM的缺点

VDOM 的优点不多说,网上的分析文章一大堆,总结起来就3点

  1. 组件的高度抽象化
  2. 可以更好的实现 SSR,同构渲染等
  3. 基于VDOM的框架可以跨平台

下面来说说 VDOM 的缺点:
首先,Virtual DOM高效是一个误解,React 从来没有说过它的 VDOM 性能很好,有的人会说Virtual DOM高效的一个理由就是它不会直接操作原生的DOM节点,因为这个很消耗性能。
当组件状态变化时它会通过某些diff算法去计算出本次数据更新真实的视图变化,然后只改变“需要改变”的DOM节点。用过React的人可能都会体会到React并没有想象中那么高效,框架有时候会做很多无用功,这体现在很多组件会被“无缘无故”进行重渲染(re-render),特别是在用了 Redux 后,这个现象会愈发明显。

所谓的re-render是你定义的class Component的render方法被重新执行,或者你的组件函数被重新执行,并不是说原生DOM被重新渲染。组件被重渲染是因为Vitual DOM的高效是建立在diff算法上的,而如果要有diff,则一定要将组件重渲染才能知道组件的新状态和旧状态有没有发生改变,从而才能计算出哪些DOM需要被更新。

这里提一下可能有的朋友会说React Fiber不是出来了吗,这个应该不是问题了吧?其实Fiber这个架构解决的问题是不让组件的重渲染和reconcile的过程阻塞主线程的执行,组件重渲染的问题依然存在,而且据反馈,React Hooks出来后组件的重渲染更加频繁了。正是因为框架本身很难避免无用的渲染,React才允许你使用一些诸如shouldComponentUpdate,PureComponent和useMemo的API去告诉框架哪些组件不需要被重渲染,可是这会引入了很多模板代码(boilerplate)。

这里引申讲下VDOM的开销在哪里, 这和它必不可少的3个步骤有关(以更新某个元素的text值为例):

  1. 调用render函数生成一颗新的VDOM
  2. 遍历元素上的新属性和旧属性,查看是否需要添加/删除/更新属性
  3. 访问到此元素,然后发现text值需要被更新,则更新

具体代码实例如下:

function MoreRealisticComponent(props) {
  const [selected, setSelected] = useState(null);

  return (
    <div>
      <p>Selected {selected ? selected.name : 'nothing'}</p>

      <ul>
        {props.items.map(item =>
          <li>
            <button onClick={() => setSelected(item)}>
              {item.name}
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}

这里 props.items 这个数据,只要state发生了变化,我们都会重新生成一堆虚拟的 li ,这是毫无意义的,这些琐屑的操作累加起来,最终对性能会造成影响,如果你想要更快的话,一个比较好的做法就是去除这些多余的操作。

核心思想

一句话总结 Svelte 的核心思想:
通过静态编译减少框架运行时的代码量
举例来说,当前的框架无论是 React、Angular 还是 Vue,不管你怎么编译,使用的时候必然需要“引入”框架本身,也就是所谓的运行时 (runtime)。但是用 Svelte 就不一样,一个 Svelte 组件编译了以后,所有需要的运行时代码都包含在里面了,除了引入这个组件本身,你不需要再额外引入一个所谓的框架运行时.
当然,这不是说 Svelte 没有运行时,但是出于下面2个原因,这个代价可以变得很小:

  1. Svelte 的编译风格是将模板编译为命令式 (imperative) 的原生 DOM 操作。
// 模板代码(编译前)
<a>{{ msg }}</a>

// 运行代码(编译后)
function renderMainFragment ( root, component, target ) {
	var a = document.createElement( 'a' );
  
	var text = document.createTextNode( root.msg );
	a.appendChild( text );
	
	target.appendChild( a )

	return {
		update: function ( changed, root ) {
			text.data = root.msg;
		},
		teardown: function ( detach ) {
			if ( detach ) a.parentNode.removeChild( a );
		}
	};
}
  1. 对于特定功能,Svelte 依然有对应的运行时代码,比如组件逻辑,if/else 切换逻辑,循环逻辑等等…但它在编译时,如果一个功能没用到,相应的代码就不会被编译到结果里去,这个有点类似于 webpack 的 Tree Shaking。

优缺点

说完了核心思想,接下来讲讲 Svelte 的优缺点吧。

优点

这里我认为最重要的优点是这3个:

  1. 完成同样功能所需的代码量相对较少
    Svelte支持静态编译,无需引入框架自身,所有需要的运行时代码都包含在里面了,除了引入这个组件本身,你感觉不到框架存在,同时 Svelte 提供了一些简单好用的模板,使得维护和编写代码都变得比较简单,相对于react,开发同样功能的代码大概要少30%(以 RealWorld 为准)
  2. 在一些不那么复杂的场景下拥有更高的性能
    Svelte 编译之后的源码几乎与手写原生代码相同,对于大多数纯展示性页面 + 少量交互的场景,其实VDOM能发挥作用的地方不多,这个时候 Svelte 能很好地发挥出优势来。
  3. 制作独立分发的小组件时有得天独厚的优势
    因为生成的组件没有runtime,没有额外的依赖,所以所有组件都可以单独使用,可以无缝地在React、Vue或Angular等其他框架里直接import
// SvelteComponent.js 是已经编译后的组件
import SvelteComponent from './SvelteComponent';

const app = new SvelteComponent({
    target: document.body
});

缺点

  1. 在一些case下存在很多重复代码
    虽然在简单的 demo 里面代码量确实非常小,但同样的组件模板,这样的 imperative 操作生成的代码量会比 vdom 渲染函数要大,多个组件中会有很多重复的代码。项目里的组件越多,代码量的差异就会逐渐缩小。同时,并不是真正的如宣传的那样 “没有 runtime“,而是根据你的代码按需 import 而已。使用的功能越多,Svelte 要包含的“运行时”代码也越多。
  2. 在大型应用中的性能还有待观察
    在大量动态内容和嵌套组件的情况下,Svelte 的更新策略决定了它也需要类似 React 的 shouldComponentUpdate 的机制来防止过度更新。另一方面,其性能优势比起现在的主流框架并不是质的区别,现在大部分主流框架的性能都可以做到 vanilla js 的 1.2~1.5 倍慢,基于 Virtual DOM 的 Inferno 更是接近原生,证明了 Virtual DOM 这个方向理论上的可能性,所以可以预见以后 web 的性能瓶颈更多是 DOM 本身而不是框架。
  3. 抽象能力偏弱
    Svelte 的编译策略决定了它跟 Virtual DOM 绝缘(渲染函数由于其动态性,无法像模板那样可以被可靠地静态分析),也就享受不到 Virtual DOM 带来的诸多好处,比如基于 render function 的组件的强大抽象能力,基于 vdom 做测试…

源码 && 产物剖析

说完了 Svelte 的特点,让我们进一步去分析下 Svelte 具体做了黑魔法,让他能够在不使用 VDOM 的情况下做到兼顾效率和产量去更新数据,下面以一个最常见的Hello World为例:

Svelte——不止于快_第2张图片
这里的c代表了create,m代表了mount,d代表了detach,即每个组件自身拥有一个完整的生命周期,包含了元素的创建、插入、更新和销毁,从而构建出自己的一整套体系。下面是我整理的Svelte编译后运行时的整体框架图,可以看到整个runtime的架构还是比较清晰的,事件体系通过state管理,同时页面有自己的状态管理和生命周期,且抽取出了常用的工具函数形成工具箱,按需引用,有效减少bundleSize。

运行时架构图

Svelte——不止于快_第3张图片

单次数据更新

说完了runtime架构,下面讲讲,Svelte是如何在不需要VDOM的情况下对数据做到高效更新的,这里以单次数据的更新过程为例:

  1. Svelte 会在代码编译的时候将每一个状态的改变转换为对应DOM节点的操作,从而在组件状态变化的时候快速高效地对DOM节点进行更新
  2. handleClick 里对 data 的操作,会被一个 $$invalidate 函数给包裹起来,便于日后判断
  3. 调用 safe_not_equal 函数来比对新老数据,若需要更新,则标记组件为一个脏组件,并触发组件的更新流程,且这里的更新是在浏览器的 microtask 里进行的,最大化地利用了显示器更新的原理

这个函数里的源码详见图片:
Svelte——不止于快_第4张图片

特殊的数据更新策略

Svelte——不止于快_第5张图片
对于一些比较特殊的数据,例如数组里的数据,那么不可避免地会碰到需要更新大量数组的问题,其实 Svelte 对于这类情况的处理方法与 React 和 Vue 类似,都是通过添加 key 来做优化,然后整个的更新策略我觉得是比 React 要好的,在更新时会记录下移动的距离,而不是像 React 一样按个比对和更新,详细更新过程见下图:

Svelte——不止于快_第6张图片

总结

Svelte 为我们提供了 VDOM 之外另一种可能性,通过静态编译减少框架运行时的代码量,让编译打包后的产物在完整实现功能的同时又有极高的性能和很小的体积,未来还将有很大的挖掘空间。

参考资料

  1. 《为什么VDOM是一个巨大的开销》
  2. 《写出最简的代码》
  3. 《如何看待Svelte这个前端框架》

你可能感兴趣的:(前端开发)