Virtual DOM(仮想DOM)
| 対応: | React 18(2022) |
|---|
『React』の仮想DOMは、実際のDOMツリーをJavaScriptオブジェクトとしてメモリ上に複製し、変更が生じた際に実際のDOMとの差分だけを効率的に更新する仕組みです。
仮想DOMの更新フロー
| ステップ | 概要 |
|---|---|
| ① レンダリング | 状態(state)や props が変化すると、React が新しい仮想DOMツリーをメモリ上に生成します。 |
| ② 差分検出(Diffing) | 以前の仮想DOMツリーと新しい仮想DOMツリーを比較し、変更された箇所を特定します。 |
| ③ 調整(Reconciliation) | 差分として検出された最小限の変更のみを実際のDOMへ反映します。 |
仮想DOMの特徴一覧
| 特徴 | 概要 |
|---|---|
| メモリ上で操作 | 仮想DOMはJavaScriptオブジェクトであるため、実際のDOMへのアクセスよりも高速に操作できます。 |
| 差分のみを更新 | 画面全体を再描画するのではなく、変更のあった部分だけを実際のDOMへ反映するため、無駄な描画コストを削減できます。 |
| 宣言的UI | 開発者は「どのような状態ならどう表示するか」を宣言するだけでよく、DOMの手動操作は不要です。差分適用は React が自動で行います。 |
| バッチ処理 | 複数の状態変化が連続して発生した場合、React はそれらをまとめて処理し、実際のDOM操作の回数を最小限に抑えます。 |
サンプルコード
パターン1: カウンターの値を更新する例です。仮想DOMが差分のみを更新する様子を確認できます。ボタンをクリックすると <span> 内のテキストノードだけが更新されます。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
// count が変化すると React が新旧の仮想DOMを比較し、
// span 内のテキストノードだけを実際のDOMへ反映する
return (
<div>
<p>草薙京のコンボ数: <span>{count}</span></p>
<button onClick={handleClick}>+1</button>
</div>
);
}
export default Counter;
パターン2: リストへのアイテム追加と削除を通じて、仮想DOMがリスト操作を効率的に処理する様子を確認します。
import { useState } from 'react';
const INITIAL_FIGHTERS = [
{ id: 1, name: '草薙京' },
{ id: 2, name: '八神庵' },
{ id: 3, name: 'テリー・ボガード' },
];
export default function FighterList() {
const [fighters, setFighters] = useState(INITIAL_FIGHTERS);
const [nextId, setNextId] = useState(4);
function addFighter() {
const newFighter = { id: nextId, name: `アンディ・ボガード(${nextId})` };
// 既存の fighters は変更せず、新しい配列を生成して state を更新する
// React は差分検出で新要素のみを DOM に追加する
setFighters([...fighters, newFighter]);
setNextId(nextId + 1);
}
function removeFighter(id) {
// filter で指定した id 以外の要素だけの新しい配列を生成する
setFighters(fighters.filter((f) => f.id !== id));
}
return (
<div>
<button onClick={addFighter}>ファイター追加</button>
<ul>
{fighters.map((fighter) => (
<li key={fighter.id}>
{fighter.name}
<button onClick={() => removeFighter(fighter.id)}>削除</button>
</li>
))}
</ul>
</div>
);
}
パターン3: key 属性が仮想DOMの差分検出に与える影響を確認します。固有の key があると、React はリストのどの要素が変わったかを正確に識別できます。
import { useState } from 'react';
const FIGHTERS = [
{ id: 'kyo', name: '草薙京', team: 'Hero' },
{ id: 'iori', name: '八神庵', team: 'Rivals' },
{ id: 'terry', name: 'テリー・ボガード', team: 'USA' },
{ id: 'andy', name: 'アンディ・ボガード', team: 'USA' },
{ id: 'kim', name: 'キム・カッファン', team: 'Korea' },
];
export default function App() {
const [sortByTeam, setSortByTeam] = useState(false);
const displayed = sortByTeam
? [...FIGHTERS].sort((a, b) => a.team.localeCompare(b.team))
: FIGHTERS;
return (
<div>
<button onClick={() => setSortByTeam(!sortByTeam)}>
{sortByTeam ? '登録順' : 'チーム順'} で表示
</button>
<ul>
{displayed.map((fighter) => (
// key に fighter.id(固有の値)を指定することで、
// 並び替え時に React は各要素を正確に追跡できる
<li key={fighter.id}>
{fighter.name} — {fighter.team}
</li>
))}
</ul>
</div>
);
}
仮想DOMとリアルDOMの比較
| 比較項目 | 仮想DOM | リアルDOM |
|---|---|---|
| 実体 | JavaScriptオブジェクト(メモリ上) | ブラウザが管理するツリー構造 |
| 操作コスト | 低い(メモリ内の演算のみ) | 高い(レイアウト計算・再描画が伴う) |
| 更新単位 | 差分のみを抽出して渡す | 変更を受け取り画面へ反映する |
| 直接操作 | 開発者からは直接操作しない | 必要に応じて ref 経由で直接操作できる。 |
概要
仮想DOMは『React』の中核をなす概念であり、高速で効率的なUI更新を実現するための仕組みです。状態や props が変化するたびに新しい仮想DOMを生成し、直前のスナップショットとの差分(差分検出アルゴリズム)を求めて、変更が必要な最小限のDOM操作だけを実際のブラウザDOMへ適用します。
この仕組みにより、開発者はDOMを直接操作する必要がなくなり、「現在の状態に対して画面がどう見えるべきか」を宣言的に記述するだけでUIを構築できます。複雑な状態変化が発生しても、React が差分計算とバッチ処理を自動的に行うため、手動で最適化コードを書く手間が省けます。
なお、React 18 以降では差分計算と描画を優先度に応じて分割処理する『Concurrent Mode』が導入されており、仮想DOMの仕組みをさらに発展させた形でパフォーマンスの改善が図られています。
よくあるミス
key 属性を忘れてリストの再レンダリングが非効率になる
リストをレンダリングする際に key 属性を指定しないと、React はどの要素が変化したかを識別できず、リスト全体を再レンダリングします。要素の挿入・削除・並び替えが多い場合にパフォーマンスが低下します。また、入力欄などのフォーカス状態が意図せずリセットされることもあります。
key_ng.jsx
// key がないと React が警告を出し、差分検出が非効率になる
function FighterList({ fighters }) {
return (
<ul>
{fighters.map((fighter) => (
<li>{fighter.name}</li>
))}
</ul>
);
}
修正後:
key_ok.jsx
// 各要素に固有の key を指定することで効率的な差分検出が可能になる
function FighterList({ fighters }) {
return (
<ul>
{fighters.map((fighter) => (
<li key={fighter.id}>{fighter.name}</li>
))}
</ul>
);
}
state を直接変更して仮想 DOM の差分検出が機能しない
state を直接変更しても React は変化を検知できません。仮想DOMの差分検出は新旧の state の参照を比較するため、同じオブジェクトを変更してもトリガーされません。必ず新しいオブジェクト・配列を生成して setState に渡す必要があります。
direct_mutation_ng.jsx
function App() {
const [fighters, setFighters] = useState(['草薙京', '八神庵']);
function addFighter() {
// state を直接変更しても React は再レンダリングをトリガーしない
fighters.push('テリー・ボガード');
setFighters(fighters); // 同じ配列参照なので差分なしと判断される
}
return <button onClick={addFighter}>追加</button>;
}
修正後:
direct_mutation_ok.jsx
function App() {
const [fighters, setFighters] = useState(['草薙京', '八神庵']);
function addFighter() {
// スプレッド構文で新しい配列を生成し、参照が変わるので差分検出が機能する
setFighters([...fighters, 'テリー・ボガード']);
}
return <button onClick={addFighter}>追加</button>;
}
key に配列インデックスを使って誤った更新が起きる
配列のインデックスを key に使うと、要素の挿入・削除・並び替え時に React が誤った要素を再利用することがあります。たとえば先頭に要素を追加した場合、インデックスが全要素でずれるため、React は全ての要素が変化したと判断し、DOM を全て作り直します。さらに入力欄のようなフォーカスを持つ要素では、意図しない要素にフォーカスが移ることがあります。
index_key_ng.jsx
// インデックスを key にするとリストの変更時に誤動作する可能性がある
{fighters.map((fighter, index) => (
<li key={index}>{fighter.name}</li>
))}
修正後:
index_key_ok.jsx
// 各要素に固有の ID を key として使うことで正確な差分検出が機能する
{fighters.map((fighter) => (
<li key={fighter.id}>{fighter.name}</li>
))}
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。