仮想DOMは純粋なオーバーヘッドです
「仮想DOMは高速である」という神話を永久に葬り去りましょう
ここ数年でJavaScriptフレームワークを使用したことがあるなら、「仮想DOMは高速である」という言葉を聞いたことがあるでしょう。これは、多くの場合、*実際の* DOMよりも高速であることを意味します。これは驚くほど根強いミームです。たとえば、Svelteは仮想DOMを使用していないのに、なぜ高速なのかと疑問を持つ人がいます。
詳しく見てみましょう。
仮想DOMとは?
多くのフレームワークでは、このシンプルなReactコンポーネントのように、render()
関数を作成することでアプリを構築します。
function function HelloMessage(props: any): div
HelloMessage(props: any
props) {
return <type div = /*unresolved*/ any
div className="greeting">Hello {props: any
props.name}</div>;
}
JSXを使わずに同じことができます...
function function HelloMessage(props: any): any
HelloMessage(props: any
props) {
return React.createElement('div', { className: string
className: 'greeting' }, 'Hello ', props: any
props.name);
}
...しかし結果は同じです。ページがどのように見えるべきかを表すオブジェクトです。そのオブジェクトが仮想DOMです。アプリの状態が更新されるたびに(たとえば、name
プロパティが変更されたとき)、新しい仮想DOMが作成されます。フレームワークの役割は、新しい仮想DOMと古い仮想DOMを*調整*して、必要な変更を把握し、実際のDOMに適用することです。
このミームはどのように始まったのですか?
仮想DOMのパフォーマンスに関する誤解された主張は、Reactの登場にまで遡ります。Reactコアチームの元メンバーであるPete Huntによる2013年の重要な講演であるRethinking Best Practicesで、私たちは次のことを学びました。
これは実際には非常に高速です。主な理由は、ほとんどのDOM操作が遅い傾向があるためです。DOMのパフォーマンスに関する多くの作業が行われてきましたが、ほとんどのDOM操作はフレームをドロップする傾向があります。

しかし、ちょっと待ってください!仮想DOM操作は、最終的な実際のDOM操作に*加えて*行われます。高速になる唯一の方法は、効率の低いフレームワーク(2013年にはたくさんありました!)と比較するか、誰も実際には行わないことをするというのが代替案であるという、架空の敵と戦う場合だけです。
onEveryStateChange(() => {
var document: Document
document.Document.body: HTMLElement
Specifies the beginning and end of the document body.
body.InnerHTML.innerHTML: string
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」に変更されたとします。
- どちらのスナップショットにも単一の要素が含まれています。どちらの場合も
<div>
なので、同じDOMノードを保持できます。 - 古い
<div>
と新しい<div>
のすべての属性を列挙して、変更、追加、または削除が必要なものがあるかどうかを確認します。どちらの場合も、属性は1つだけです。値が"greeting"
のclassName
です。 - 要素に降りていくと、テキストが変更されていることがわかるので、実際のDOMを更新する必要があります。
これらの3つの手順のうち、この場合は3番目の手順にのみ価値があります。なぜなら、圧倒的多数の更新の場合と同様に、アプリの基本構造は変更されていないからです。手順3に直接スキップできれば、はるかに効率的です。
if (changed.name) {
text.data = const name: void
name;
}
(これは、Svelteが生成する更新コードとほぼ同じです。従来のUIフレームワークとは異なり、Svelteは*実行時*に作業を行うのを待つのではなく、*ビルド時*にアプリで何が変更される可能性があるかを知っているコンパイラです。)
差分検出だけではありません
Reactや他の仮想DOMフレームワークで使用されている差分検出アルゴリズムは高速です。おそらく、より大きなオーバーヘッドはコンポーネント自体にあります。このようなコードは書かないでしょう...
function function StrawManComponent(props: any): p
StrawManComponent(props: any
props) {
const const value: any
value = expensivelyCalculateValue(props: any
props.foo);
return <type p = /*unresolved*/ any
p>the const value: any
value is {const value: any
value}</p>;
}
...なぜなら、props.foo
が変更されたかどうかに関係なく、すべての更新でvalue
を不注意に再計算することになるからです。しかし、はるかに良性に見える方法で、不必要な計算と割り当てを行うことは非常に一般的です。
function function MoreRealisticComponent(props: any): div
MoreRealisticComponent(props: any
props) {
const [const selected: any
selected, const setSelected: any
setSelected] = useState(null);
return (
<type div = /*unresolved*/ any
div>
<type p = /*unresolved*/ any
p>Selected {const selected: any
selected ? const selected: any
selected.name : 'nothing'}</p>
<type ul = /*unresolved*/ any
ul>
{props: any
props.items.map((item: any
item) => (
<type li = /*unresolved*/ any
li>
<type button = /*unresolved*/ any
button onClick={() => const setSelected: any
setSelected(item)}>{item: any
item.name}</button>
</li>
))}
</ul>
</div>
);
}
ここでは、props.items
が変更されたかどうかに関係なく、すべての状態変更で、それぞれにインラインイベントハンドラを持つ仮想<li>
要素の新しい配列を生成しています。パフォーマンスに病的にこだわっていない限り、それを最適化することはありません。意味がありません。十分に高速です。しかし、もっと速くなる方法を知っていますか?*それをしないことです。*
些細な作業であっても、不必要な作業をデフォルトで行うことの危険性は、アプリが最終的に「千の傷による死」に屈し、最適化が必要になったときに目指すべき明確なボトルネックがないことです。
Svelteは、そのような状況に陥らないように明示的に設計されています。
では、なぜフレームワークは仮想DOMを使用するのですか?
仮想DOMは*機能ではない*ことを理解することが重要です。それは目的を達成するための手段であり、目的は宣言型の状態駆動型UI開発です。仮想DOMは、状態遷移を考慮せずにアプリを構築できるため、*一般的に十分な*パフォーマンスを実現できるため、価値があります。これは、バグの少ないコードを意味し、退屈なタスクではなく、創造的なタスクに費やす時間を増やすことを意味します。
しかし、仮想DOMを使用せずに同様のプログラミングモデルを実現できることがわかりました。そこでSvelteが登場します。