⚛️ React Boundary
内部構造 — アーキテクチャ

🌳 Virtual DOMの真実

「Virtual DOMを使うから速い」——これは誤解です。 Virtual DOMが本当に解決しようとした問題は何か、その設計思想を解き明かします。

🤔 問題提起:Virtual DOMは本当に速いのか?

2013年にReactが登場したとき、「Virtual DOMによって高速なUI更新を実現」という説明が広まりました。 多くのブログや記事が「DOMを直接操作するより、Virtual DOMを経由した方が速い」と主張しました。

これは正確ではありません

Virtual DOMにはオーバーヘッドがあります:

  • JavaScriptオブジェクト(Virtual DOM)を生成するコスト
  • 前回のVirtual DOMと新しいVirtual DOMを比較するdiffコスト
  • 差分をDOMに反映するcommitコスト

純粋なDOM操作(element.textContent = '新しい値')は、 Virtual DOMを使わない分だけ原理的に速いです。

では、なぜReactはVirtual DOMを採用したのでしょうか? 「速いから」ではないとしたら、本当の理由は何なのでしょうか?

🎯 結論から言う

Virtual DOMの本質は「抽象化レイヤー」です。

「速さ」ではなく「予測可能性」「抽象化」「クロスプラットフォーム」のために存在します。

🎯
予測可能性

stateからUIを計算する際、Reactは「今のDOMがどんな状態か」を気にしません。 「stateがこれなら、UIはこうなるべき」という宣言から、差分を計算します。

🧩
抽象化

開発者はDOMを直接操作しません。JSXを書くと、Reactが最適なDOM操作を行います。 「何をするか(What)」だけ宣言すればよく、「どうDOMを変えるか(How)」は不要です。

📱
クロスプラットフォーム

Virtual DOMはDOMという概念に依存しません。同じJSXから、 React DOMはブラウザのDOMを生成し、React NativeはiOS/AndroidのネイティブUIを生成します。

🗺️ 構造図:Virtual DOMとは何か

Virtual DOMは、実際のDOM要素を模したJavaScriptオブジェクトのツリーです。

JSX → Virtual DOM → 実際のDOM
① JSXを書く
const element = (
  <div className="container">
    <h1>Hello</h1>
    <p className="text">World</p>
  </div>
);
↓ Babelが変換(React.createElement)
② Virtual DOMオブジェクト(React要素)
// React.createElement("div", { className: "container" }, ...)
{
  type: "div",
  props: {
    className: "container",
    children: [
      { type: "h1", props: { children: "Hello" } },
      { type: "p",  props: { className: "text", children: "World" } }
    ]
  },
  key: null,
  ref: null
}
↓ ReactDOMが生成(初回)またはdiff後に更新
③ 実際のDOMノード
<div class="container">
<h1>Hello</h1>
<p class="text">World</p>
</div>

📚 概念説明:Virtual DOMが生まれた背景

2013年当時の問題

React登場前のフロントエンド開発では、JavaScriptフレームワーク(AngularJS、Backbone.jsなど)が 使われていましたが、それぞれに課題がありました。

AngularJS(双方向データバインディング)の問題

モデルとビューの双方向バインディングは便利でしたが、 状態の変化をDOMに反映する「Dirty Checking」が、大規模UIでは重くなりました。 また、「どこで何が変わったのか」を追うのが困難でした。

手動DOM操作(jQuery)の問題

DOM操作を手動で行うと、アプリが大きくなるほど「どのDOMがどの状態に対応するか」の 管理が複雑になります。あちこちにDOM操作が散らばり、バグの温床になりました。

「全部再描画すればいい」というアイデア

Reactを作ったFacebookのエンジニアたちは大胆な発想を持ちました: 「stateが変わったら、UIを全部描き直せばいい」。

// 概念的なアイデア
function render(state) {
  // stateから完全なUIを計算(毎回全部!)
  return (
    <div>
      <h1>{state.title}</h1>
      <ul>
        {state.items.map(item => <li key={item.id}>{item.name}</li>)}
      </ul>
      <p>Total: {state.items.length}</p>
    </div>
  );
}

// これが「宣言的UI」の核心
// 問題: 毎回DOMを全部破壊して再構築するのは遅すぎる
// 解決: Virtual DOMで「何が変わったか」を計算してから、最小限のDOM操作

Virtual DOMはトレードオフ

Virtual DOMは「全部再計算する宣言的アプローチ」と「効率的なDOM操作」を両立するための トレードオフです。JavaScriptオブジェクト(Virtual DOM)の操作はDOMの操作より何倍も速い。 だから「仮想的なツリーで計算してから、最小限のDOMを変更する」が成立します。

💻 コード例:diffアルゴリズムの概要

2つのVirtual DOMツリーを比較して差分を求めることをreconciliation(差分検出)と呼びます。 一般的なdiffアルゴリズムはO(n³)のコストがかかりますが、 ReactはUIの特性を活かしてO(n)まで削減しています。

Reactのdiff戦略

戦略1:異なるタイプの要素は完全に置き換える
// Before
<div>
  <Counter />
</div>

// After(divからspanに変わった)
<span>
  <Counter />
</span>

// Reactは差分を探さず、divツリー全体を破棄して
// spanツリーを新しく構築する(Counter もアンマウント・再マウント)
戦略2:同じタイプなら属性だけ更新
// Before
<div className="old" style={{ color: 'red' }}>Hello</div>

// After(classNameだけ変わった)
<div className="new" style={{ color: 'red' }}>Hello</div>

// Reactはdivを再利用して className だけ更新する
// element.className = 'new'; // これだけ実行される
戦略3:リストはkeyで識別する
// keyなし(先頭への挿入で全要素が更新される!)
// Before: [A, B, C]
// After:  [Z, A, B, C]
// Reactは A→Z, B→A, C→B, 新規→C と判断 → 非効率

// keyあり(正しく識別される)
// Before: [<li key="a">A</li>, <li key="b">B</li>]
// After:  [<li key="z">Z</li>, <li key="a">A</li>, <li key="b">B</li>]
// Reactは key="z" が新規追加、key="a" key="b" は移動と判断 → 効率的

🔧 仕組み分解:FiberとVirtual DOMの関係

React 16以降、Virtual DOMの実装としてFiberというデータ構造が使われています。 「Virtual DOM」という言葉は概念を指し、「Fiber」はその実装です。

Virtual DOM と Fiber の関係
React要素(Virtual DOM)

JSXから生成される軽量なJavaScriptオブジェクト。 { type, props, key }という構造。 毎回新しく作られる「不変のスナップショット」。

Fiber(作業単位)

コンポーネントの実行状態を保持する長命なオブジェクト。 stateや副作用、スケジューリング情報を持つ。 再利用される「ミュータブルなインスタンス」。

// 処理の流れ
// 1. JSX → React要素(Virtual DOM)が生成される
const element = <Counter count={5} />;
// { type: Counter, props: { count: 5 }, key: null }

// 2. Reactが既存のFiberと新しいReact要素を比較(diff)
// current Fiber: { type: Counter, memoizedProps: { count: 3 }, ... }
// new element:   { type: Counter, props: { count: 5 } }
// → propsが変わった → Fiberを更新する

// 3. 差分をcommit phaseでDOMに反映
// counterElement.setAttribute('data-count', '5') など

Virtual DOMの「速さ」の誤解を整理する

誤解

「Virtual DOMはDOMより速いので、常に高速なUIが実現できる」

正確

「Virtual DOMはJavaScriptオブジェクトなのでDOMより操作が速い。 宣言的UIによる『全部再計算』のコストを、 効率的なdiffによって許容できるレベルに抑えている」

本質

Virtual DOMの真の価値は「DOMを直接操作せずに済む抽象化」によって、 宣言的UIプログラミングモデルを可能にすること。 速さは副次的な特性。

クロスプラットフォームが可能な理由

// 同じJSXが異なるプラットフォームに対応できる理由

// react-dom(ブラウザ)
// Virtual DOM → document.createElement('div') など

// react-native(モバイル)
// Virtual DOM → <View>, <Text> などのネイティブUI

// react-three-fiber(3D)
// Virtual DOM → Three.jsのメッシュ・ジオメトリ

// ink(CLI)
// Virtual DOM → ターミナルへのテキスト出力

// 核心: ReactはVirtual DOMの「差分」を計算するだけ
// 「その差分をどこに反映するか」は renderer が担当
// → 同じコンポーネントコードがどこでも動く

🏁 Virtual DOMを超えて:Compiler時代

近年、「Virtual DOMは本当に必要か?」という議論も起きています。 SvelteやSolidJSのようなフレームワークは、Virtual DOMを使わずに コンパイル時に「どのDOMを変えるか」を解析し、直接DOM操作するコードを生成します。

// Svelte のアプローチ(概念)
// コンパイル時に「countが変わったらp要素を更新」と解析

// コンパイル後(概念的なイメージ)
let count = 0;
const p = document.createElement('p');
p.textContent = count;

function setCount(newValue) {
  count = newValue;
  p.textContent = count; // ← コンパイラが生成した直接操作
  // Virtual DOMのdiffは不要!
}

// React Compiler(React 19)も同様のアプローチで
// 不要な再レンダリングをコンパイル時に削除しようとしている

React Compilerの登場

React 19に向けて開発中のReact Compilerは、 コンパイル時に不要な再レンダリングを自動的に排除します。 これによりuseMemouseCallbackを 手動で書く必要がなくなります。Virtual DOMは引き続き使われますが、 再レンダリング自体が減るため実質的な最適化が行われます。

📌 まとめ

  • ✓ Virtual DOMは「DOMより速い」わけではなく、オーバーヘッドがある
  • ✓ Virtual DOMの本当の価値は「予測可能性」「抽象化」「クロスプラットフォーム」
  • ✓ 宣言的UIで「全部再計算」する際のコストを、diffアルゴリズムで許容範囲に収めている
  • ✓ ReactのdiffはO(n)に最適化:異なるタイプは全替え、同タイプは属性更新、リストはkeyで識別
  • ✓ Fiberは「Virtual DOM」という概念の現代的な実装(中断可能、優先度付き)
  • ✓ React RendererがVirtual DOMの差分をDOM・ネイティブ・CLIなど各環境に反映する

関連記事