"虚拟 DOM 很快"这样的神话或许不一定是对的
如果你近些年正在使用主流的前端框架,你可能会听说操作虚拟DOM比操作真实DOM更快这样的言论。人们也很好奇Svelte是如何做到在不使用虚拟DOM技术的情况下做到性能极佳的。
本篇文章我们就来一起聊聊。
什么是虚拟DOM
在许多前端框架中,你可能会通过render
函数来渲染你的应用程序,就像下面这样:
tsx
function HelloMessage(props) {
return (
<div className="greeting">
Hello {props.name}
</div>
);
}
或者不使用jsx也可以做到:
js
function HelloMessage(props) {
return React.createElement(
'div',
{ className: 'greeting' },
'Hello ',
props.name
);
}
总的来说结果是一样的,你都会得到一个用于描述UI页面的javascript对象,这个对象就是虚拟DOM。任何时候你的组件状态发生改变(例如当上面例子中属性name
发生该改变的时候),就会重新创建一个新的虚拟DOM对象。框架的工作就是对比新旧两个虚拟DOM,然后找到哪里发生了变化,然后把变化的部分映射成为真实的DOM。
这样的策略明智吗?
这种关于虚拟DOM的性能神话的误解的来源需要追溯到React在Rethinking Best Practices的发布会上,那个一个在2013年由React团队成员Pete Hunt的发布的一个研讨上提出的,他说:
这实际上非常快,主要是因为大多数 DOM 操作往往很慢。在 DOM 上做了很多性能工作,但大多数 DOM 操作都容易丢帧。
但请稍等!虚拟 DOM 操作是对真实 DOM 的最终操作的补充。它可能更快的唯一方法是,如果我们将其与效率较低的框架进行比较(2013 年有很多可以解决的问题!),或者反对稻草人 - 另一种选择是做一些没人真正做过的事情:
js
onEveryStateChange(() => {
document.body.innerHTML = renderMyApp();
});
Pete然后说明道:
React 不是魔法。就像您可以使用 C 进行汇编并击败 C 编译器一样,如果您愿意,您也可以投入原始 DOM 操作和 DOM API 调用并击败 React。然而,使用 C 或 Java 或 JavaScript 可以实现一个数量级的性能改进,因为您不必担心......平台的具体情况。使用 React,您可以构建应用程序,甚至无需考虑性能,并且默认状态很快。
但这不是重点。
所以是虚拟DOM性能不好吗?
不完全是。它更像是"虚拟 DOM 通常足够快",但有一些注意事项。
React 最初的承诺是,您可以在每次状态更改时重新渲染整个应用程序,而无需担心性能。在实践中,我认为这并不准确。如果是的话,就不需要像 shouldComponentUpdate 这样的优化(这是一种告诉 React 何时可以安全地跳过组件的方法)。
即使使用 shouldComponentUpdate,一次性更新整个应用程序的虚拟 DOM 也是一项艰巨的工作。不久前,React 团队推出了一种名为 React Fiber 的东西,它允许将更新分解为更小的块。这意味着(除其他外)更新不会长时间阻塞主线程,尽管它不会减少总工作量或更新所需的时间。
那么开销来自哪里呢?
最明显的是......如果不首先将新的虚拟DOM与以前的快照进行比较,就不能对真实DOM应用更改。以前面的HelloMessage为例,假设name
从'world'更改为'everybody'。
tsx
function HelloMessage(props) {
return (
<div className="greeting">
Hello {props.name}
</div>
);
}
-
两个快照都包含一个父元素。在这两种情况下都是<div>,这意味着我们可以不必更新这个DOM节点
-
我们再检查这个元素上的的所有属性,以查看是否需要更改、添加或删除任何属性。在这两种情况下,我们都有一个单一的属性------一个className,其值为"greeting"。
-
再看子元素,我们看到文本已经改变,所以我们需要更新真正的DOM
在这三个步骤中,只有第三个步骤在这种情况下有价值,因为------就像绝大多数更新的情况一样------应用程序的基本结构没有改变。如果我们直接跳到步骤3,效率会高得多:
js
if (changed.name) {
text.data = name;
}
(这几乎正是 Svelte 生成的更新代码。与传统的 UI 框架不同,Svelte 是一个编译器,它在构建时知道应用程序中的内容会如何变化,而不是等待在运行时完成工作。)
不仅仅是diff的原因
React 和其他虚拟 DOM 框架使用的比较算法速度很快。可以说,更大的开销在于组件本身。你就不能这样写代码了...
tsx
function StrawManComponent(props) {
const value = expensivelyCalculateValue(props.foo);
return (
<p>the value is {value}</p>
);
}
..因为无论 props.foo 是否更改,您都会在每次更新时不小心重新计算值。但以看似好得多的方式进行不必要的计算和分配是非常常见的:
tsx
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>
);
}
在这里,我们在每次状态更改时生成一个新的虚拟 <li> 元素数组 - 每个元素都有自己的内联事件处理程序,无论 props.items 是否已更改。除非你对性能过分痴迷,否则你不会对其进行优化。毫无意义。速度够快了。但你知道什么会更快吗?不这样做。
React Hooks 加倍努力默认执行不必要的工作,并获得可预测的结果。 默认执行不必要的工作(即使该工作微不足道)的危险在于,您的应用程序最终将屈服于"千刀万剐",一旦需要优化,就没有明确的瓶颈可以瞄准。
Svelte 的设计目的就是为了防止您陷入这种情况。
为什么其他框架会使用虚拟DOM呢
重要的是要理解虚拟DOM不是一个特性。它是达到目的的一种手段,目的是声明式的、状态驱动的UI开发。虚拟DOM很有价值,因为它允许您构建应用程序而无需考虑状态转换,性能通常足够好。这意味着更少的错误代码,更多的时间花在创造性的任务上,而不是乏味的任务上。
但事实证明,我们可以在不使用虚拟DOM的情况下实现类似的编程模型------这就是Svelte的用武之地。