Web●●●○○

仮想DOM

かそうどむ

UIの状態をメモリ上の軽い木で持ち、差分だけを実DOMへ反映する更新戦略。Reactが広めた。

概要

仮想DOM(virtual DOM)は、UIの状態を実際の DOM ではなく、メモリ上の軽量な JavaScript オブジェクトの木として持っておき、状態が変わるたびに「新しい木」を丸ごと作り直して前回の木と比較し、差分だけを実DOMへ反映する更新戦略です。Reactが2013年に広めて以来、宣言的UIを支える代表的な仕組みとして定着しました。

開発者から見た嬉しさは、「どこを書き換えるか」を考えなくてよくなることです。状態を受け取ってUIの木を返す関数(コンポーネント)を書くだけで、「前回との差分を見つけて最小限のDOM操作に変換する」面倒な仕事はライブラリが引き受けてくれます。UIを「状態の関数」として毎回ゼロから書き下せるのに、実際の画面更新は差分だけ — この両立が仮想DOMの本質です。

React のほか Vue や Preact も仮想DOM(またはその変種)を採用しており、SPA 開発の標準的な語彙になっています。

なぜ生まれたか

仮想DOM以前のフロントエンドは、jQueryに代表される命令的なDOM操作が主流でした。「チェックボックスが変わったらこのラベルを更新し、合計欄も更新し、ボタンの活性も切り替える」— 状態が増えるほど「どの変更がどの画面箇所に波及するか」の組み合わせが爆発し、更新漏れや二重更新によるUIの不整合がバグの温床になっていました。状態とDOMという2つの真実が、手作業の同期で辛うじてつながっている状態です。

理想は「状態が変わったら画面全体を作り直す」ことです。常にゼロから描けば同期漏れは原理的に起きません。しかしDOMの再構築は重く、入力フォーカスやスクロール位置も失われるため、実DOMでこれをやるのは非現実的でした。そこでReactが持ち込んだのが、「作り直すのは軽いメモリ上の木だけにして、実DOMには差分だけ当てる」という折衷案です。開発者には「毎回全部描き直す」単純なモデルを提供しつつ、裏側で差分検出(reconciliation)が実DOM操作を最小化する — 宣言的UIを実用的な速度で成立させたのが仮想DOMでした。

詳細

更新の流れ — 状態から差分パッチまで

状態が変わると、コンポーネント関数が再実行されて新しい仮想DOMツリーが生成されます。ライブラリはメモリに保持していた前回のツリーと新しいツリーを比較(diff)し、「このノードのテキストが変わった」「この要素が追加された」といった差分のリストを作り、それだけを実DOMへパッチとして適用します。複数の状態変更は一度の更新にまとめられる(バッチング)ため、実DOMへの反映回数はさらに減ります。

状態の変更新しい仮想木を生成ABC’前回の仮想木を保持ABC2つの木を比較して差分検出実DOMへ最小限のパッチ変わったノードC’だけを書き換える
仮想DOMの更新サイクル — 木は毎回作り直すが、実DOMには差分だけ当てる

差分検出のヒューリスティクスと key

2つの木の厳密な最小差分を求めるのは計算量的に高くつくため、実装は割り切ったルールで比較します。代表的なのは「要素の種類が違えば、その下は丸ごと作り直す」「同じ種類なら属性の差分だけ更新して子へ進む」という2つです。

リストの比較には key という目印が必要になります。key がないと、ライブラリはリストを先頭から順番に突き合わせるため、先頭に1件追加しただけで全項目が「変わった」と誤認され、余計なDOM更新や入力状態の混線が起きます。各項目に安定した一意の key(多くはデータのID)を付けることで、「並びが変わっただけで同じ項目だ」と対応付けられ、移動として最小限に処理されます。配列のインデックスを key にすると並べ替えや挿入で対応付けが壊れるため、典型的な落とし穴とされています。

仮想DOMは万能ではない

誤解されがちですが、仮想DOMは「速くするための魔法」ではありません。手書きで最適なDOM操作を書いた場合と比べれば、木の再生成と差分計算はまるごと追加のオーバーヘッドです。仮想DOMが買っているのは速度ではなく、「宣言的に書いても実用上十分速い」という開発体験と保守性です。コンポーネントが大きくなれば再レンダリングと差分計算のコスト自体が問題になり、React でメモ化(memouseMemo)による再計算の抑制が頻出の最適化になるのはこのためです。

この前提を突いたのが別のアプローチです。Svelte はコンパイル時に「どの状態がどのDOMノードに影響するか」を解析し、差分検出なしで該当箇所を直接更新するコードを生成します。SolidJS はリアクティビティ(シグナルによる依存追跡)で変更箇所をピンポイントに更新し、仮想DOMを持ちません。「毎回作って比べる」か「依存を追跡して直接更新するか」は宣言的UIを実現する2大路線であり、仮想DOMはそのひとつの解だと位置づけて理解するのが現在地です。なお、仮想DOMが実DOMから独立した抽象であることは副産物も生みました。サーバ上で木を文字列として描画する SSR や、React Native のようなDOM以外への描画も、同じコンポーネントから実現できています。