🎨 Virtual DOMとは?
📖 定義
Virtual DOMは実際のDOMの軽量なコピーで、メモリ上にのみ存在するJavaScriptオブジェクトです。Reactのようなライブラリは、Virtual DOMを使用してUI変更を仮想的に処理した後、実際のDOMに最小限の変更のみを適用します。これにより、Webアプリケーションのパフォーマンスが大幅に向上します。
🎯 比喩で理解する
建築設計図の比喩
Virtual DOMを建築設計図に例えると:
実際の建物 (Real DOM)
├─ 建築費用が高い
├─ 修正に時間がかかる
└─ 解体して建て直すのが難しい
設計図 (Virtual DOM)
├─ 紙の上で自由に修正
├─ 速くて安価
├─ 複数案の比較が可能
└─ 最終案だけを実際の建物に反映
プロセス:
1. 設計図で何度も修正 (Virtual DOM)
2. 変更された部分のみを把握
3. 実際の建物に最小限だけ適用 (Real DOM)
こうすることで建築費用と時間を節約!
ワードプロセッサの比喩
メモ帳 (バニラJavaScript)
- タイピングするたびにすぐに保存
- 遅くて非効率的
- ファイル入出力の繰り返し
ワードプロセッサ (React + Virtual DOM)
- メモリ上で編集
- 変更を追跡
- 保存ボタンを押したときだけファイルに書き込み
- 速くて効率的
Virtual DOM = メモリバッファ
Real DOM = ディスクに保存
ゲームレンダリングの比喩
ゲーム開発では:
直接画面に描画 (Real DOM直接操作)
- ちらつきが発生
- パフォーマンス低下
- 部分更新が難しい
ダブルバッファリング (Virtual DOM方式)
1. 裏画面(バッファ)に絵を描く
2. 完成したら表画面と交換
3. スムーズなレンダリング
4. 変更されたピクセルだけを更新
Virtual DOMがダブルバッファリングと同じ役割!
⚙️ 動作原理
1. DOMとは?
// DOM (Document Object Model)
// HTMLをJavaScriptで操作できるインターフェース
<!DOCTYPE html>
<html>
<body>
<div id="root">
<h1>Hello</h1>
<p>World</p>
</div>
</body>
</html>
// DOMツリー
document
└─ html
└─ body
└─ div#root
├─ h1 ("Hello")
└─ p ("World")
// JavaScriptでDOM操作
document.getElementById('root').innerHTML = '<h1>Changed!</h1>';
2. Real DOMの問題点
// ❌ 非効率的なReal DOM操作
// 問題1: 毎回全体の再レンダリング
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
document.body.appendChild(div); // 1000回DOM操作!
}
// 問題2: レイアウト再計算 (Reflow)
element.style.width = '100px'; // Reflow発生
element.style.height = '100px'; // Reflow発生
element.style.margin = '10px'; // Reflow発生
// 問題3: 複雑なUI更新
// 一文字変わっても全体コンポーネント再生成
<div>
<Header />
<Content /> ← これだけが変わったのに
<Footer />
</div>
// 全体を作り直さなければならない!
// Real DOM操作が遅い理由:
// 1. ブラウザレンダリングエンジンとJavaScriptエンジン間の通信
// 2. Reflow (レイアウト再計算)
// 3. Repaint (画面の再描画)
3. Virtual DOM動作過程
// ✅ Virtual DOMの効率的な処理
// 1段階: 初期レンダリング
const initialVirtualDOM = {
type: 'div',
props: { id: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Count: 0'] },
{ type: 'button', props: {}, children: ['Click'] }
]
};
// Real DOMに反映
<div id="app">
<h1>Count: 0</h1>
<button>Click</button>
</div>
// 2段階: 状態変更 (count = 1)
const newVirtualDOM = {
type: 'div',
props: { id: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Count: 1'] }, // ← 変更された
{ type: 'button', props: {}, children: ['Click'] }
]
};
// 3段階: Diffing (差分計算)
const diff = {
path: ['div', 'h1', 'text'],
oldValue: 'Count: 0',
newValue: 'Count: 1'
};
// 4段階: Reconciliation (再調整)
// 最小限の変更だけReal DOMに適用
document.querySelector('h1').textContent = 'Count: 1';
// divとbuttonはそのまま維持!
4. Diffingアルゴリズム
// ReactのDiffingアルゴリズムの原理
// ルール1: 異なるタイプの要素は異なるツリーを生成
// 前
<div>
<Counter />
</div>
// 後
<span>
<Counter />
</span>
// 結果: divをspanに完全に置き換え
// Counterもアンマウント後、再マウント
// ルール2: keyを使用してリストアイテムを追跡
// ❌ keyなし
<ul>
<li>A</li>
<li>B</li>
</ul>
// 先頭にCを追加
<ul>
<li>C</li> // 全体を作り直す
<li>A</li>
<li>B</li>
</ul>
// ✅ key使用
<ul>
<li key="a">A</li>
<li key="b">B</li>
</ul>
// 先頭にCを追加
<ul>
<li key="c">C</li> // Cだけ追加
<li key="a">A</li> // そのまま
<li key="b">B</li> // そのまま
</ul>
// ルール3: 再帰的に子を比較
function diff(oldNode, newNode) {
// 1. タイプが異なれば置き換え
if (oldNode.type !== newNode.type) {
return { action: 'REPLACE', newNode };
}
// 2. 属性を比較
const propsDiff = diffProps(oldNode.props, newNode.props);
// 3. 子を比較 (再帰)
const childrenDiff = diffChildren(
oldNode.children,
newNode.children
);
return { propsDiff, childrenDiff };
}
5. Reconciliation (再調整)
// Reconciliation: Virtual DOMをReal DOMに反映
// Phase 1: Render Phase (非同期)
// - Virtual DOM比較
// - 変更事項を計算
// - 中断可能 (React 18 Fiber)
// Phase 2: Commit Phase (同期)
// - Real DOM更新
// - ライフサイクルメソッド実行
// - 中断不可能
// 例: リスト更新
const oldList = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 3, name: 'C' }
];
const newList = [
{ id: 1, name: 'A' },
{ id: 3, name: 'C-updated' }, // 修正
{ id: 4, name: 'D' } // 追加
// id: 2 削除
];
// Reconciliation結果:
// - id:1 維持
// - id:2 削除
// - id:3 テキストだけ更新
// - id:4 追加
// 実際のDOM操作を最小化:
// remove(id:2)
// updateText(id:3, 'C-updated')
// append(id:4)
💡 実際の例
基本Virtual DOM実装
// 簡単なVirtual DOM実装 (教育用)
// 1. Virtual DOMノード生成
function createElement(type, props = {}, ...children) {
return {
type,
props,
children: children.flat()
};
}
// JSXなしで使用
const vdom = createElement(
'div',
{ id: 'app', className: 'container' },
createElement('h1', {}, 'Hello Virtual DOM'),
createElement('p', {}, 'This is a paragraph')
);
console.log(vdom);
// {
// type: 'div',
// props: { id: 'app', className: 'container' },
// children: [
// { type: 'h1', props: {}, children: ['Hello Virtual DOM'] },
// { type: 'p', props: {}, children: ['This is a paragraph'] }
// ]
// }
// 2. Virtual DOMをReal DOMに変換
function render(vnode) {
// テキストノード
if (typeof vnode === 'string' || typeof vnode === 'number') {
return document.createTextNode(vnode);
}
// 要素ノード
const element = document.createElement(vnode.type);
// 属性を適用
Object.entries(vnode.props || {}).forEach(([key, value]) => {
if (key === 'className') {
element.setAttribute('class', value);
} else if (key.startsWith('on')) {
// イベントリスナー
const event = key.substring(2).toLowerCase();
element.addEventListener(event, value);
} else {
element.setAttribute(key, value);
}
});
// 子をレンダリング (再帰)
(vnode.children || []).forEach(child => {
element.appendChild(render(child));
});
return element;
}
// 使用
const domElement = render(vdom);
document.body.appendChild(domElement);
Diffingアルゴ リズム実装
// 3. Diffingアルゴリズム (簡単バージョン)
function diff(oldVNode, newVNode) {
// 1. 新しいノードがなければ削除
if (!newVNode) {
return { type: 'REMOVE' };
}
// 2. 以前のノードがなければ追加
if (!oldVNode) {
return { type: 'CREATE', newVNode };
}
// 3. タイプが異なれば置き換え
if (
typeof oldVNode !== typeof newVNode ||
(typeof oldVNode === 'string' && oldVNode !== newVNode) ||
oldVNode.type !== newVNode.type
) {
return { type: 'REPLACE', newVNode };
}
// 4. 同じタイプなら更新
if (newVNode.type) {
return {
type: 'UPDATE',
props: diffProps(oldVNode.props, newVNode.props),
children: diffChildren(oldVNode.children, newVNode.children)
};
}
// 5. 変更なし
return { type: 'NONE' };
}
// 属性を比較
function diffProps(oldProps = {}, newProps = {}) {
const patches = [];
// 変更または追加された属性
Object.keys(newProps).forEach(key => {
if (oldProps[key] !== newProps[key]) {
patches.push({ type: 'SET_PROP', key, value: newProps[key] });
}
});
// 削除された属性
Object.keys(oldProps).forEach(key => {
if (!(key in newProps)) {
patches.push({ type: 'REMOVE_PROP', key });
}
});
return patches;
}
// 子を比較
function diffChildren(oldChildren = [], newChildren = []) {
const patches = [];
const maxLength = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < maxLength; i++) {
patches.push(diff(oldChildren[i], newChildren[i]));
}
return patches;
}
Patch適用
// 4. PatchをReal DOMに適用
function patch(parent, patches, index = 0) {
if (!patches) return;
const element = parent.childNodes[index];
switch (patches.type) {
case 'CREATE':
parent.appendChild(render(patches.newVNode));
break;
case 'REMOVE':
parent.removeChild(element);
break;
case 'REPLACE':
parent.replaceChild(render(patches.newVNode), element);
break;
case 'UPDATE':
// 属性を更新
patches.props.forEach(propPatch => {
if (propPatch.type === 'SET_PROP') {
element.setAttribute(propPatch.key, propPatch.value);
} else if (propPatch.type === 'REMOVE_PROP') {
element.removeAttribute(propPatch.key);
}
});
// 子を更新 (再帰)
patches.children.forEach((childPatch, i) => {
patch(element, childPatch, i);
});
break;
}
}
ReactでVirtual DOMを使用
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// Reactが自動的にVirtual DOMを処理
return (
<div className="counter">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
</div>
);
}
// 内部動作:
// 1. count変更 → setState呼び出し
// 2. Reactが新しいVirtual DOMを生成
// 3. 以前のVirtual DOMと比較 (Diffing)
// 4. <h1>テキストだけが変更されたことを検出
// 5. Real DOMのテキストノードだけを更新
// 6. div、buttonはそのまま維持
// パフォーマンス比較:
// ❌ バニラJS: 全体div.innerHTMLを置き換え
// ✅ React: <h1>のテキストだけを更新
複雑なリストの例
import React, { useState } from 'react';
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '運動する', done: false },
{ id: 2, text: '勉強する', done: false },
{ id: 3, text: '掃除する', done: false }
]);
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<h1>やることリスト</h1>
<ul>
{todos.map(todo => (
<li
key={todo.id} // keyはDiffing最適化に必須!
style={{
textDecoration: todo.done ? 'line-through' : 'none',
color: todo.done ? '#888' : '#000'
}}
>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>削除</button>
</li>
))}
</ul>
</div>
);
}
// Virtual DOMの効率性:
// todoを1つだけチェックしても
// - ❌ 全体リストの再生成 (X)
// - ✅ 該当<li>のstyleだけ変更 (O)
// keyの重要性:
// keyがあれば:
// - アイテムの移動/削除時に効率的
// - DOMの再利用が可能
// - コンポーネントの状態を維持
// keyがなければ:
// - 全体リストの再生成
// - パフォーマンス低下
// - コンポーネントの状態損失の可能性
パフォーマンス最適化例
import React, { useState, useMemo, memo } from 'react';
// memoでコンポーネントのメモ化
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
console.log(`TodoItem ${todo.id} レンダリング`);
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>削除</button>
</li>
);
});
function OptimizedTodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '運動する', done: false },
{ id: 2, text: '勉強する', done: false },
{ id: 3, text: '掃除する', done: false }
]);
// useMemoで計算結果をキャッシュ
const activeTodos = useMemo(() => {
console.log('アクティブなやることを計算');
return todos.filter(todo => !todo.done);
}, [todos]);
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<h1>やることリスト</h1>
<p>残りのやること: {activeTodos.length}個</p>
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</ul>
</div>
);
}
// 最適化の効果:
// 1. memo: todoを1つだけ変更すれば、該当TodoItemだけ再レンダリング
// 2. useMemo: todosが変更されたときだけactiveTodosを再計算
// 3. key: 効率的なリスト更新
// パフォーマンス比較:
// 最適化前: 10個のtodoのうち1個変更 → 10個すべて再レンダリング
// 最適化後: 10個のtodoのうち1個変更 → 1個だけ再レンダリング
Virtual DOMなし vs あるときの比較
// ❌ Virtual DOMなし (バニラJavaScript)
function updateWithoutVirtualDOM() {
const app = document.getElementById('app');
let count = 0;
// 毎回全体HTMLを再生成
setInterval(() => {
count++;
app.innerHTML = `
<div class="container">
<h1>Count: ${count}</h1>
<p>Current time: ${new Date().toLocaleTimeString()}</p>
<button>Click me</button>
</div>
`;
// 問題点:
// 1. 全体DOMツリーの再生成
// 2. イベントリスナーの消失
// 3. 入力中のinputの初期化
// 4. スクロール位置の初期化
// 5. アニメーションの中断
}, 1000);
}
// ✅ Virtual DOMあるとき (React)
function CounterWithVirtualDOM() {
const [count, setCount] = useState(0);
const [time, setTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
setTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div className="container">
<h1>Count: {count}</h1>
<p>Current time: {time.toLocaleTimeString()}</p>
<button onClick={() => alert('Clicked!')}>Click me</button>
</div>
);
}
// 利点:
// 1. <h1>と<p>のテキストだけを更新
// 2. イベントリスナーを維持
// 3. コンポーネントの状態を維持
// 4. スムーズな更新
// 5. 最小限のDOM操作
実際のパフォーマンス測定
import React, { useState } from 'react';
function PerformanceComparison() {
const [items, setItems] = useState([]);
const [renderTime, setRenderTime] = useState(0);
const generateItems = (count) => {
const startTime = performance.now();
const newItems = Array.from({ length: count }, (_, i) => ({
id: i,
value: Math.random()
}));
setItems(newItems);
const endTime = performance.now();
setRenderTime(endTime - startTime);
};
return (
<div>
<h1>パフォーマンス比較</h1>
<div>
<button onClick={() => generateItems(100)}>
100個生成
</button>
<button onClick={() => generateItems(1000)}>
1000個生成
</button>
<button onClick={() => generateItems(10000)}>
10000個生成
</button>
</div>
<p>レンダリング時間: {renderTime.toFixed(2)}ms</p>
<div style={{ maxHeight: '400px', overflow: 'auto' }}>
{items.map(item => (
<div key={item.id} style={{ padding: '5px', borderBottom: '1px solid #eee' }}>
Item #{item.id}: {item.value.toFixed(4)}
</div>
))}
</div>
</div>
);
}
// 結果 (一般的な場合):
// バニラJS (全体innerHTML):
// - 100個: ~50ms
// - 1000個: ~500ms
// - 10000個: ~5000ms (カクつき)
// React (Virtual DOM):
// - 100個: ~10ms
// - 1000個: ~100ms
// - 10000個: ~1000ms (スムーズ)
// Virtual DOMのパフォーマンス向上:
// 初期レンダリングは似ているが、
// 更新時に5-10倍速い!