🌳 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オブジェクトのツリーです。
const element = (
<div className="container">
<h1>Hello</h1>
<p className="text">World</p>
</div>
); // 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
} 📚 概念説明:Virtual DOMが生まれた背景
2013年当時の問題
React登場前のフロントエンド開発では、JavaScriptフレームワーク(AngularJS、Backbone.jsなど)が 使われていましたが、それぞれに課題がありました。
モデルとビューの双方向バインディングは便利でしたが、 状態の変化をDOMに反映する「Dirty Checking」が、大規模UIでは重くなりました。 また、「どこで何が変わったのか」を追うのが困難でした。
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戦略
// Before <div> <Counter /> </div> // After(divからspanに変わった) <span> <Counter /> </span> // Reactは差分を探さず、divツリー全体を破棄して // spanツリーを新しく構築する(Counter もアンマウント・再マウント)
// 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'; // これだけ実行される // 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」はその実装です。
JSXから生成される軽量なJavaScriptオブジェクト。
{ type, props, key }という構造。
毎回新しく作られる「不変のスナップショット」。
コンポーネントの実行状態を保持する長命なオブジェクト。 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は、
コンパイル時に不要な再レンダリングを自動的に排除します。
これによりuseMemoや
useCallbackを
手動で書く必要がなくなります。Virtual DOMは引き続き使われますが、
再レンダリング自体が減るため実質的な最適化が行われます。
📌 まとめ
- ✓ Virtual DOMは「DOMより速い」わけではなく、オーバーヘッドがある
- ✓ Virtual DOMの本当の価値は「予測可能性」「抽象化」「クロスプラットフォーム」
- ✓ 宣言的UIで「全部再計算」する際のコストを、diffアルゴリズムで許容範囲に収めている
- ✓ ReactのdiffはO(n)に最適化:異なるタイプは全替え、同タイプは属性更新、リストはkeyで識別
- ✓ Fiberは「Virtual DOM」という概念の現代的な実装(中断可能、優先度付き)
- ✓ React RendererがVirtual DOMの差分をDOM・ネイティブ・CLIなど各環境に反映する