仮想DOMは純粋なオーバーヘッド(Virtual DOM is pure overhead)
‘仮想DOMは速い’という神話を完全に終わりにしよう
ここ数年でJavaScriptフレームワークを使ったことがある人なら、’仮想DOMは速い’ というフレーズを聞いたことがあるでしょう、これはしばしば、実際のDOMよりも速い、という意味で言われることがあります。これは驚くほどしぶといミームです — 例えば、どうやってSvelteは仮想DOMを使わずに高速にできるのかを尋ねられることがありました。
では、じっくり見ていきましょう。
仮想DOMとは?
多くのフレームワークで、render()
関数を作ってアプリを構築します。例えばシンプルな React コンポーネントでは:
function function HelloMessage(props: any): boolean
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です。アプリのstateが更新されるたびに(例えば name
prop が変わったとき)、これが新たに作成されます。フレームワークの仕事は、新しいオブジェクトと古いオブジェクトを 調整 し、どのような変更が必要か把握して、実際のDOMにそれを適用することです。
このミームはどう始まった?
仮想DOMのパフォーマンスに関する誤解された主張は、Reactの立ち上げまで遡ります。元ReactコアチームメンバーのPete Hunt氏による2013年の発展的な講演 Rethinking Best Practices で、私たちは次のことを学びました。
これは実際には非常に高速で、主な理由は、ほとんどのDOM操作は遅くなる傾向があるからです。DOMには多くのパフォーマンス作業がありますが、ほとんどのDOM操作はフレームをドロップする傾向があります。
※原文 : This is actually extremely fast, primarily because most DOM operations tend to be slow. There’s been a lot of performance work on the DOM, but most DOM operations tend to drop frames.
しかし、ちょっと待ってください! 仮想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を使うことで、パフォーマンスを気にすることなくアプリケーションを構築することができますし、デフォルトの state は高速です。
※原文 : React is not magic. Just like you can drop into assembler with C and beat the C compiler, you can drop into raw DOM operations and DOM API calls and beat React if you wanted to. However, using C or Java or JavaScript is an order of magnitude performance improvement because you don’t have to worry...about the specifics of the platform. With React you can build applications without even thinking about performance and the default state is fast.
…しかし、それは行き詰まった部分ではありません。
それで…仮想DOMは遅い?
その表現は正しくありません。’仮想DOMは大抵、十分に速い’というほうがより近いですが、いくつかの注意点があります。
Reactの当初の約束は、パフォーマンスを心配することなく、state が1つ変更されるたびにアプリ全体を再レンダリングできる、というものでした。実際には、それは正確ではないと思います。もしそうなら、shouldComponentUpdate
(コンポーネントを安全にスキップできるときにReactに伝える方法) のような最適化は必要ないはずです。
shouldComponentUpdate
を使ったとしても、アプリ全体の仮想DOMを一度に更新するのは大変な作業です。しばらく前に、ReactチームはReact Fiberと呼ばれるものを導入し、更新をより小さなチャンクに分割できるようになりました。これは (とりわけ) 更新によってメインスレッドが長時間ブロックされないことを意味しますが、総作業量や更新にかかる時間が減るわけではありません。
オーバーヘッドはどこから?
ほぼ間違いなく、差分検出のコストはゼロではありません(原文 : diffing isn’t free)。まず仮想DOMとその直前のスナップショットの比較をしないと、変更を実際のDOMに適用できません。先ほどの HelloMessage
の例で言えば、name
propが ‘world’ から ‘everybody’ に変わったとします。
- どちらのスナップショットにも単一の要素が含まれています。どちらの場合もそれは
<div>
であり、同じ DOM ノードを維持できることを意味します。 - 古い
<div>
と新しい<div>
のすべての属性を列挙して、変更、追加、削除する必要があるか調べます。どちらも、値が"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>
要素の新しい配列(それぞれがインラインのイベントハンドラを持つ)をそれぞれの状態が変化するたびに生成しています。よっぽどパフォーマンスにこだわっていない限り、これを最適化することはないでしょう。意味がありません。これで十分に速いのですから。しかし、さらに速い方法がわかりますか? こうしないことです 。
デフォルトで不必要な作業を行うことは危険で、たとえその作業が些細なものであっても、最適化の際に明確なボトルネックがないためにアプリがやがて ‘じわじわと破滅に向かう’(原文 : death by a thousand cuts)ことに屈してしまいます。
Svelteは、そのような状況に陥らないよう明示的に設計されています。
では、なぜフレームワークは仮想DOMを使うのか?
重要なのは、仮想DOMは 機能ではない ということです。それは目的を達成するための手段であり、その目的とは宣言的で状態駆動型のUI開発です。仮想DOMは、状態遷移を考えることなくアプリケーションを開発できるようにし、 一般的には十分な パフォーマンスを得られるという点で価値があります。つまり、バグを減らし、退屈な作業ではなく創造的な作業に多くの時間を費やすことができるようになります。
しかし、仮想DOMを使用せずに同様のプログラミングモデルを実現できることがわかりました — つまりSvelteの登場です。