[译]Virtual DOM is pure overhead

1,412 阅读6分钟

虚拟DOM是纯粹的浪费

让我们彻底地破除“虚拟DOM很快”的神话吧。

原文地址:svelte.dev/blog/virtua…

如果您在过去几年中使用过JavaScript框架,那么您可能听说过“虚拟DOM很快”这句话,通常说它比真正的DOM快。这是一个令人惊讶的弹性模仿行为——例如,有人问,如果不使用虚拟DOM,那么Svelte的速度有多快。

是时候仔细看看了。

什么是虚拟DOM?

在许多框架中,您可以通过创建render()函数来构建应用程序,就像这个简单的react组件:

function HelloMessage(props) {
  return (
    <div className="greeting">
      Hello {props.name}
    </div>
  );
}

不用JSX你也可以做同样的事情...

function HelloMessage(props) {
  return React.createElement(
    'div',
    { className: 'greeting' },
    'Hello ',
    props.name
  );
}

但是结果都是一样的,一个表示页面现在应该是什么样子的对象。这个对象就是虚拟DOM。每次应用程序的状态更新时(例如,当名称属性变化时),都会创建一个新的。框架的工作是协调新的和旧的,找出需要更改的地方并将它们应用到真正的DOM中。

模仿是怎么开始的?

关于虚拟DOM性能的误解可以追溯到React的发布。2013年,React前核心团队成员Pete Hunt的一次重要演讲中,‘重新思考最佳做法’的演讲中,我们了解到以下内容:

这实际上非常快,主要是因为大多数DOM操作都比较慢。在DOM上做了很多性能工作,但是大多数DOM操作都会对框架造成负影响。

但是等一下!虚拟DOM操作是对真实DOM的最终操作的补充。唯一可以更快的方法是,我们将其与效率较低的框架进行比较(2013年还有很多工作要做!),或者与一个无足轻重的人争论,认为另一种选择是做一些没人会做的事情:

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”。

  1. 两个快照都包含一个元素。在这两种情况下都是<div>,这意味着我们可以保持相同的DOM节点
  2. 我们枚举旧的<div>和新的<div>上的所有属性,看看是否需要更改、添加或删除任何属性。在这两种情况下,我们都有一个属性——一个值为“greeting”的类名
  3. 下降到元素中,我们看到文本已经更改,因此我们需要更新真正的DOM

在这三个步骤中,只有第三个步骤在本例中有价值,因为-就像绝大多数更新一样-应用程序的基本结构保持不变。如果我们直接跳到第3步,效率会更高:

if (changed.name) {
  text.data = name;
}

(这几乎正是Svelte生成的更新代码。与传统的UI框架不同,Svelte是一个编译器,它在构建时知道应用程序中的情况如何变化,而不是等待在运行时完成工作。)

但这不仅仅是比较

React和其他虚拟DOM框架使用的比较算法是快速的。按理说,组件本身的开销更大。你应该不会写这样的代码...

function StrawManComponent(props) {
  const value = expensivelyCalculateValue(props.foo);

  return (
    <p>the value is {value}</p>
  );
}

因为无论props.foo是否已更改,每次更新时都会不小心重新计算值。但以更为友好的方式进行不必要的计算和分配是非常常见的:

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是否已更改。除非你极度地追求性能,否则你不会去优化它。没有意义。足够快了。但你知道什么会更快吗?不必要这么做。

即使不必要的工作是微不足道的,但如果你不去做这些工作,那么你的应用程序最终会在没有明显障碍的情况下不停地失败,而现在是时候进行优化了。

Svelte是意图鲜明的设计,以防止你结束在这种情况。

那么为什么框架使用虚拟DOM呢?

理解虚拟DOM不是一个特性很重要。它是达到目的的一种手段,目的是声明性的、状态驱动的UI开发。虚拟DOM很有价值,因为它允许您构建应用程序,而无需考虑状态转换,性能通常足够好。这意味着更少的错误代码,更多的时间花在创造性的任务上,而不是单调乏味的任务上。

但事实证明,我们可以在不使用虚拟DOM的情况下实现类似的编程模型,这就是Svelte的用武之地。