September 9, 2023

虚拟 DOM 的性能到底如何

虚拟 DOM 的性能到底如何

讨论虚拟 DOM 之前,需要了解 命令式代码、声明式代码

1 命令式和声明式

前端开发中,存在两种代码方式:

1.1 命令式

命令是的一大特点就是,关注过程

例如,我现在要为 div 设置文本内容,并添加点击事件

1
2
3
const div = document.getElementById('container');   // 获取 Dom
div.innerText = 'New Word';
div.addEventListener('click', () => {console.log('触发了点击事件')})

可以看到,使用原生 JS,关注于一步一步的过程:

1.2 声明式

而声明式的特点是,关注结果

结合 Vue ,来实现上面的功能

1
<div @click="() => {console.log('触发了点击事件')}"> New Word </div>

这种代码,不关心具体是怎样实现的,不需要获取 Dom 来做进一步的操作,我们只关心结果

相当于告诉 Vue:”帮我给 div 节点加上文本内容,添加上点击事件“

而具体的过程,是 Vue 封装好

所以:Vue 暴漏给用户的是 声明式,内部是 命令式

1.3 命令式和声明式的优缺点

在复杂页面开发的情况下,相比于声明式,命令式显然书写相对繁琐一点。显然使用声明式代码可维护性较强一些。

那么命令式和声明式的性能谁更优呢?

声明式代码的性能不一定优于命令式代码的性能

例如上面的示例:

命令式代码,直接使用命令操作,修改文本内容:

1
div.innerText = 'New Word';  // 直接修改

声明式代码:

1
2
3
4
<!-- 之前: -->
<div> 旧文本 </div>
<!-- 之后: -->
<div> New Word </div>

使用声明式代码,其实框架需要找到前后的差异,只更新变化的地方,然后执行 div.innerText = 'New Word'; 进行修改,完成修改文本内容

如果将修改的性能消耗定义为 A,把找出差异的性能消耗定义为 B,可以得出如下:

这符合前面提到的:声明式代码的性能不一定优于命令式代码的性能

但是如果能将 B 的性能消耗降低,无限接近 0,声明式和命令式的性能消耗就能相等

而能够降低性能 B 的方式,就是虚拟 DOM 了

所谓的虚拟 DOM,就是最小化找出差异的性能消耗,使声明式代码性能消耗 接近 命令式代码的性能消耗。

2 虚拟 DOM 创建页面的性能消耗

为了比较虚拟 DOM 的性能

需要了解其创建、更新的过程

我们都知道,虚拟 DOM 实际上是 JS 对象,所以:

虚拟 DOM 创建页面的效率 = 创建 JS 对象的效率 + 新建所有 DOM 元素的效率

虚拟 DOM 创建页面的效率
纯 JS 层面计算 创建 JS 对象的效率
DOM 层面计算 新建所有 DOM 元素的效率

为了比较纯 JS 层面的计算消耗,这里使用 JavaScript 代码执行效率对比工具进行测试

github:https://github.com/barretlee/performance

上面是纯 JS 层面的计算,模拟创建虚拟 DOM,循环 1000 次

下面则是 DOM 操作,循环 1000 次,每创建一个对象,作为子节点插入

根据 Relative Rate,纯 JS 层面的计算的效率要远远高于DOM 操作

从宏观上来看,则认为 虚拟 DOM 创建页面的效率 和 直接采用真实 DOM ,是没有差异的

2 虚拟 DOM 更新页面的性能消耗

上面讨论了虚拟 DOM 创建页面时的性能情况,或许可以认为和直接操作真实DOM没有差异,甚至深究下,虚拟 DOM 的性能更差

但是在更新界面的情况下,性能的提升就明显了

如果我们使用 innerHtml 更新界面

1
div.innerHTML = 'New Word'

其过程是,重新构建 HTML 字符串,再重新设置 DOM 元素的 innerHTML 的属性

也就是:那么我们只改了一个字,也要销毁所有旧的 DOM 元素,再全量创建新的 DOM 元素

而虚拟 DOM 更新页面得过程:

它需要重新构建 JS 对象(虚拟DOM),通过 Diff 算法,寻找差异,复用旧 DOM,只找到更新的元素进行修改

虚拟 DOM innerHTML
纯 JS 层面计算 创建 JS 对象的效率 + Diff 渲染 HTML 字符串
DOM 层面计算 仅更新变化的 DOM 销毁所有旧 DOM + 新建所有新 DOM

即使虚拟 DOM 多出 构建 JS 对象(虚拟DOM),通过 diff 算法的两个步骤

但他们都是 纯 JS 层面的计算,经过上面的测试,不会产生数量级的消耗

再观察 DOM 层面的消耗

虚拟 DOM 仅更新必要的元素

但 innerHTML 需要全量更新,页面内容越多,性能消耗越大

这时虚拟 DOM 的优势就体现出来了

3 总结

根据命令式和声明式代码的原理,我们能得知:

声明式代码的更新性能消耗 = 修改的性能消耗定义 + 找出差异的性能消耗

可以认为虚拟 DOM 能够使声明式代码性能消耗 接近 命令式代码的性能消耗

经过测试纯 JS 层面计算消耗和 DOM 层面的计算消耗,能确定在宏观角度上,虚拟 DOM 在创建页面的消耗差异不大,在更新界面的情况下,如果页面内容越多,获得的性能提升也会更好

同时,纯 JS 层面的少量计算消耗,与获得较好的代码维护性上来看,虚拟 DOM 是一个不错的选择

参考:《vue.js设计与实现》霍春阳 著作

About this Post

This post is written by Duan WeiCheng, licensed under CC BY-NC 4.0.