Web●●●○○

仮想DOM

Virtual DOM ・ かそうどむ

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

✦ 30秒で試す — 航海最適な手書きの画面操作より、実はひと手間多い仕組みだ先頭に1件足したとき、画面の部品に起きる書き換えは?予想してみる

概要

仮想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)へパッチとして適用します。複数の状態変更は一度の更新にまとめられる(バッチング)ため、実DOMへの反映回数はさらに減ります。

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

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

2つの木の厳密な最小差分(木の編集距離)を求めると、ノード数 n に対して O(n³) の計算がかかり、毎回の更新には使えません。そこで React は割り切ったルールで O(n) に落としています。第一に「要素の種類が違えば、その下は丸ごと作り直す」(<div> が <span> に変わったら、子孫を比べずに捨てて作る)。第二に「同じ種類なら属性の差分だけ更新して子へ進む」。第三に「兄弟の並びは、開発者が付けた key で対応づける」です。

3つめのルールが実務でいちばん効きます。key がないと、ライブラリはリストを先頭から位置で突き合わせます。3件のリストの先頭に1件足すと、位置 0 の古い行は位置 0 の新しい行と比べられ、「文字が変わった」と判定されます。結果として全行の文字が書き換えられ、末尾に1行が新しく作られます。無駄が多いだけでなく、入力欄の中身やフォーカス、スクロール位置のように、仮想DOMが知らない「実DOMの中にだけある状態」は元の位置に残るため、別の項目の隣に並んでしまいます。各項目に安定した一意の key(多くはデータの ID)を付けると、ライブラリは「同じ項目が動いただけ」と分かり、実DOMの行をそのまま移動します。

配列の添字を key にするのは、key を付けないのと同じ結果になります(React は key がないと警告を出したうえで添字を使います)。並べ替えも挿入もしない固定のリストなら問題ありませんが、動くリストでは典型的な落とし穴です。逆に、key を毎回 Math.random() で作ると、どの行も「別の項目」と判定されて全部作り直しになります。

下の部品で確かめてください。行の ⋮⋮ をつかんで並べ替えたり先頭に追加したりすると、実DOMに当たったパッチが光ります。key を「インデックス」にしたまま並べ替えると、メモが別の果物の隣に混線します。

⚡ 体験: 行を並べ替えて、key が差分検出に何を教えるかを見る

実DOM(買い物メモ)

  • ⋮⋮りんご
  • ⋮⋮みかん
  • ⋮⋮ぶどう

新しい仮想DOMの木(前回の木と突き合わせる)

<ul><li key=0>"りんご"<input><li key=1>"みかん"<input><li key=2>"ぶどう"<input>
key
文字の書き換え0移動0生成0混線した行0
文字の書き換え行の移動行の生成混線

行の左の ⋮⋮ をつかんで並べ替えるか、「先頭に追加」を押してください。入力欄のメモが、ちゃんと自分の果物についていくかを見ます。

仮想DOMは万能ではない

誤解されがちですが、仮想DOMは「速くするための魔法」ではありません。手書きで最適な DOM 操作を書いた場合と比べれば、木の再生成と差分計算はまるごと追加のオーバーヘッドです。仮想DOMが買っているのは速度ではなく、「宣言的に書いても実用上十分速い」という開発体験と保守性です。コンポーネントが大きくなれば再レンダリングと差分計算のコスト自体が問題になり、React では memo や useMemo による再計算の抑制が長らく頻出の最適化でした。2025年に安定版になった React Compiler は、このメモ化をビルド時に自動で挿入するもので、「作り直す範囲を減らす」作業を人の手から道具へ移しています。

React 自身も差分検出の実行方法を変えてきました。2017年の Fiber(React 16)で、木の比較を小さな単位に分けて中断・再開できるようにし、入力への応答のような急ぎの更新を先に処理できるようになりました。差分の考え方は同じまま、「いつ比べるか」を賢くした改良です。

もう一つの路線 — 依存を追跡して直接更新する

仮想DOMの前提を突いたのがリアクティビティの路線です。SolidJS はシグナル(読まれた値を記録する入れ物)で依存を追跡し、変わった値を読んでいる DOM ノードだけを直接書き換えるので、仮想DOMを持ちません。Svelte は、4 まではコンパイル時に「どの変数がどの DOM に影響するか」を解析して更新コードを生成する方式、2024年の Svelte 5 からは runes($state など)で実行時にシグナルを使う方式で、どちらも仮想DOMの差分検出を使いません。Vue も 3.6 に向けて、仮想DOMを通さずに DOM を直接操作する Vapor モードを用意しています。「毎回作って比べる」か「依存を追跡して直接更新するか」は宣言的UIを実現する2大路線であり、仮想DOMはそのひとつの解だと位置づけるのが現在地です。

なお、仮想DOMが実DOMから独立した抽象であることは副産物も生みました。サーバ上で木を HTML の文字列として描き出す SSR や、React Native のような DOM 以外への描画も、同じコンポーネントから実現できています。

試問

画面の左は実際に表示されている買い物メモのリストで、3行(りんご・みかん・ぶどう)にそれぞれメモが入っています。右は新しい仮想DOMの木で、前回の木と突き合わせた結果、実DOMに当たった操作(文字の書き換え・行の移動・行の生成)が光ります。下で、各行に付ける名札(key)を「なし・インデックス・ID」から選べ、「先頭に追加」で1件足せます。

✦ 命題挑戦中

リストの先頭に1件足したとき、実DOMへの操作を「行を1つ作る」だけにし、メモを混線させるな

  • リストの先頭に1件追加する
  • そのときの実DOMへの操作が「生成1回」だけ(書き換え・移動は0)
  • メモが別の果物の隣に混線していない
⚡ 体験: 行を並べ替えて、key が差分検出に何を教えるかを見る

実DOM(買い物メモ)

  • ⋮⋮りんご
  • ⋮⋮みかん
  • ⋮⋮ぶどう

新しい仮想DOMの木(前回の木と突き合わせる)

<ul><li key=0>"りんご"<input><li key=1>"みかん"<input><li key=2>"ぶどう"<input>
key
文字の書き換え0移動0生成0混線した行0
文字の書き換え行の移動行の生成混線

行の左の ⋮⋮ をつかんで並べ替えるか、「先頭に追加」を押してください。入力欄のメモが、ちゃんと自分の果物についていくかを見ます。