仮想 DOM と差分更新(Reconciliation)

仮想 DOM が解く問題、再レンダーから実 DOM 反映までの流れ、key と状態の対応を理解する。

仮想 DOM は、画面にある実 DOM の複製ではありません。多くの UI フレームワークが、ある状態で UI がどうあるべきかを JavaScript の軽量な木として表し、前回の結果と比較して実 DOM への変更を決めるための仕組みです。宣言的 UI を実現する有力な方法ですが、性能を自動的に保証する魔法ではありません。

再レンダーは実 DOM の全作り直しではない

React では state 更新などにより再レンダーが起こると、コンポーネント関数を呼んで次の UI の記述を求めます。その後、前回の結果と照合し、必要な DOM 操作を commit します。

state 変更
  → 次の UI ツリーを計算する(render)
  → 前回のツリーと対応付ける(reconcile)
  → 必要な実 DOM 変更を反映する(commit)
  → ブラウザが描画する

「コンポーネントが再レンダーした」ことと「対応する DOM が必ず変更された」ことは同義ではありません。親が再レンダーしても、子の出力が同じで最終的な DOM 操作が不要なことはあります。反対に、DOM 操作が少なくても、重い JavaScript 計算やブラウザの layout が遅ければ UI は遅くなります。

差分比較は一般問題を完全には解けない

二つの任意の木の最小編集差分を求めることは高コストになり得ます。そのためフレームワークは、開発者が構造と識別子を適切に表すことを前提に、実用的な規則で対応付けます。要素の型が変われば古い部分木を置き換える、同じ位置に同じ型があれば更新候補とみなす、といった規則です。

この近似が UI 開発で有効なのは、画面の構造が通常は大きく変わらず、リスト項目のような移動する対象には識別子を渡せるためです。差分計算の詳細を実装者が毎回書かなくてよい代わりに、構造と identity を正しく伝える責任があります。

key は配列の位置ではなく、項目の同一性を表す

リストでは安定した key を使います。

function TodoList({ todos }: { todos: Todo[] }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <label><input type="checkbox" checked={todo.done} /> {todo.title}</label>
        </li>
      ))}
    </ul>
  );
}

key は「この位置にあるもの」ではなく「この todo はこの todo である」を表します。配列の index を key にして途中へ挿入・並べ替えをすると、フレームワークは別の項目を同じものとして再利用する可能性があります。その結果、入力欄の値、フォーカス、子コンポーネントの state が違う行へ移ったように見えます。

表示順が絶対に変わらず、追加・削除もない静的な配列では index が問題にならないこともあります。しかしデータに本来の ID があるなら、それを key にするのが安全な既定です。レンダーごとに Math.random() を生成する key は毎回別物と伝えるため、再利用を妨げます。

identity は state の保持範囲を決める

仮想 DOM における対応付けは、DOM ノードだけでなくコンポーネントの state をどこまで保つかにも関わります。同じ位置で同じコンポーネントが続けば state を保ち、型や key が変われば新しいものとして扱えます。

これは意図的なリセットにも使えます。

<EditProfileForm key={user.id} user={user} />

ユーザーを切り替えたら編集途中のローカル state も初期化したい、という意味を key で表しています。ただし、単に再レンダーを強制する目的で key を変えるのは避けます。なぜ状態をリセットする必要があるのかを設計として説明できるときに使います。

仮想 DOM のコストと最適化

仮想 DOM を使うと、各更新で JavaScript のツリー作成と比較が発生します。ほとんどの業務 UI では、まず状態の置き場所とコンポーネント構造を正しくする方が重要です。memo、useMemo、computed、手動の比較などを先に増やすと、古い値を表示するバグや複雑な依存関係を作ることがあります。

最適化を検討するのは、プロファイリングで繰り返し重いレンダーが確認できた後です。候補には次があります。

  • 大量リストでは画面に見える範囲だけを描く仮想化
  • 重い計算を入力の変化時だけ再計算する
  • state を適切な場所へ移し、無関係なツリーの更新を減らす
  • key とコンポーネント分割を見直し、不要な作り直しを防ぐ

まとめ

仮想 DOM は、状態から次の UI を計算し、実 DOM への変更を整理するための中間表現です。再レンダーと DOM の全再作成を混同せず、リストでは項目の identity を安定した key で表すことが大切です。次は仮想 DOM を使う場合も使わない場合も含め、フレームワークごとの DOM 更新戦略を比較します。

Sources