仮想DOMを理解したい
ReactなどのモダンJavaScriptライブラリ・フレームワークを学習していると避けては通れない仮想DOM。
仮想DOMとはどんなもので、なにがいいのかなどを解説していきます。
そもそもDOM(Document Object Model)とはなに?
HTML、CSS、JavaScriptを勉強したら必ずでてくるDOM。
ツリー構造のオブジェクトなどの説明がよくされています。
よくある勘違いとして「DOMとHTMLは同じもの」があるのかなと思います。
ドキュメントオブジェクトモデル (Document Object Model, DOM) は、ウェブページを表す HTML のような文書の構造をメモリー内に表現することで、ウェブページとスクリプトやプログラミング言語を接続するものです。
DOMはHTMLじゃない
DOMはHTMLそのものではなく、HTMLをブラウザがパース(解析)した結果(オブジェクトモデル) です。
簡単に言うと、HTMLを設計図として「実際に建てられた家」のようなものです。
ブラウザの開発者ツールでElememntsという、「HTMLのようなもの」が表示されますが、これはブラウザがHTMLを解析して作成したDOMツリーを表示したもの(ライブビュー)です。
HTMLソースコードを見ているのではなく、JavaScriptやCSSの変更が反映された現在のDOMの状態になります。
<!DOCTYPE html>
<html>
<head>
<title>サンプル</title>
</head>
<body>
<div id="app">
<h1>タイトル</h1>
<p>内容です</p>
</div>
</body>
</html>
ブラウザがHTMLからDOMをつくる流れ
1. HTMLファイルをダウンロード(設計図を取得)
↓
2. HTMLを読み取る
↓
3. DOMを作る ← ここで「操作できる仕組み」ができる
↓
4. CSSでスタイルを決める
↓
5. どこに何を置くか計算する
↓
6. 実際に画面に表示する
DOMがあるとなにができるのか
実はDOMはJavaScriptの登場により誕生しました。
1990年代にいわゆるブラウザ戦争があり、各ブラウザが独自の規格を採用していたため、「このブラウザでは表示できるけど、このブラウザでは表示できない」なんてことがよくありました。
現在はW3Cによって仕様が統一され、細かい挙動の違いはありますが、ブラウザによって表示が大きく違うことはなくなりました。
そんな時期にJavaScriptが登場するのですが、DOMはJavaScriptのためのAPIとして設計されました。
つまり、DOMは「Webページに動的な機能を追加したい」というニーズに対応して生まれたもので、単にHTMLをブラウザが解析しただけのものではありません。
🤔 もしDOMがなかったら?
HTMLはプログラミング言語ではなく、静的なマークアップ言語であり、構造を記述するだけです。
JavaScriptはDOMがなければ、HTML要素にアクセスできないためユーザーの操作によってWebページを変化させることはできません。
つまり、ほぼテキストを表示するだけの静的なページしか作れません。
<html>
<body>
<h1>会社概要</h1>
<p>私たちの会社は...</p>
</body>
</html>
<html>
<body>
<h1 id="title">こんにちは!</h1>
<button id="colorButton">色を変える</button>
</body>
</html>
<script>
// 要素を取得
const title = document.getElementById('title');
const button = document.getElementById('colorButton');
// ボタンがクリックされたら色を変える
button.addEventListener('click', function() {
title.style.color = 'red';
});
</script>
DOMの登場によって、動的なWebページを作ることができるようになりましたが、DOMには「操作が重い」や「状態管理が困難」などの欠点がありました。
なぜDOMは重いのか
- 1回のDOM操作につき、ブラウザが大量に処理を実行
- レイアウトの再計算
- 画面の再描画
- 複数のDOM操作をすることで、重い処理が何度も実行される
// 1行目:即座にDOM操作が発生
document.getElementById('title').textContent = 'New Title';
// ↓ ブラウザが即座に実行
// - 要素を探す
// - テキストを変更
// - レイアウト再計算
// - 画面再描画
// 2行目:また即座にDOM操作が発生
document.getElementById('content').textContent = 'New Content';
// ↓ ブラウザが再び実行
// - 要素を探す
// - テキストを変更
// - レイアウト再計算
// - 画面再描画
// 3行目:また即座にDOM操作が発生
document.getElementById('count').textContent = '42';
// ↓ ブラウザが3回目の実行
// - 要素を探す
// - テキストを変更
// - レイアウト再計算
// - 画面再描画
// 結果:重い処理が3回発生
DOM操作をするたびに一行ずつ処理が実行されるため、スライダーなどの連続した処理をする場合、どうしてもカクツキが目立ってしまうことがあります。GSAPやSwiperなどのライブラリを使用した際に、滑らかになるはずなのに動きが滑らかにならない、といった経験をされた方もいるのではないでしょうか。
状態管理と差分管理の難しさ
説明した通り、DOMはライブビューのため、「現在の状態」しか保持しません。
つまり過去の状態や変更履歴はわからないため、どこで何が変更されたかを追跡するのは困難です。
また、「何を更新するべきか」を手動で管理する必要があります。
そのため更新漏れやバグが発生したり、その特定がしにくい。
条件分岐も複雑になりがちです。
仮想DOMとは
これらのDOMの欠点を解決するのが仮想DOMです。
仮想DOMという概念は、Reactの開発元であるFacebook(現Meta)によって2013年に公開されました。
仮想DOMを取り入れたライブラリ・フレームワークだとReact、Vue.jsが有名です。
先にお伝えしておくと、
仮想DOMはDOMの代わりではありません。 仮想DOMは設計図のようなものであり、最終的に実際のDOMが生成されるのは変わりません。
仮想DOM=JavaScriptオブジェクト
仮想DOMはただのJavaScriptオブジェクトです。
// 実際のHTML
<div id="app">
<h1>タイトル</h1>
<p>内容です</p>
</div>
// 仮想DOM(JavaScriptオブジェクト)
const virtualDOM = {
type: 'div',
props: { id: 'app' },
children: [
{
type: 'h1',
props: {},
children: ['タイトル']
},
{
type: 'p',
props: {},
children: ['内容です']
}
]
};
従来のDOMの生成はHTMLファイル、CSSファイル、JavaScriptファイルから生成されていました。
HTML/CSS/JavaScript → DOM
仮想DOMは、従来のHTML、CSS、JavaScriptの役割がすべて含まれており、仮想DOMからDOMを生成します。
仮想DOM(設計図) → DOM → 結果
つまり、DOMが使われなくなったわけではなく、DOMの生成方法が違うということになります。
// 仮想DOM: すべてを一箇所で管理
function render(state) {
return {
type: 'div',
props: {
id: 'app',
style: { padding: '20px' } // CSS相当
},
children: [
{
type: 'h1',
props: {
style: { color: 'blue', fontSize: '24px' } // CSS相当
},
children: [state.title] // HTML相当
},
{
type: 'button',
props: {
onClick: handleClick // JavaScript相当
},
children: ['クリック']
}
]
};
}
仮想DOMのレイアウト変更
仮想DOMのレイアウト変更は、設計図の比較をするところからはじまります。
- 古い仮想DOMと新しいDOMを見比べて、違いがある箇所を特定します
- 違いが特定できたら、その部分だけを実際のDOMに反映します
従来の方法
「タイトルを変更して」→ すぐ工事
「テキストの色を赤に変えて」→ すでに赤だった(無駄)
「ボタンを無効化して」→ すぐ工事
→ 3回の作業、1回は無駄
仮想DOMの方法
1. 設計図を比較
2. 「タイトル変更」「ボタン無効化」のみ必要と判明
3. 2つの作業をまとめて効率的に実行
→ 無駄な作業なし、効率的
重要なポイント
- 変更がない部分は触らない
- 変更がある部分だけを特定
- まとめて実行
これにより、従来のDOM操作よりもユーザーのパソコンに負担が少なく滑らかに動作します。
仮想DOMで履歴管理ができる理由
- 不変性 - オブジェクトを変更せず、新しく作成
- 軽量性 - JavaScriptオブジェクトなのでメモリ効率が良い
- 参照保持 - 古いオブジェクトが自動的に保持される
- 構造化 - 状態の変化を構造的に記録できる
不変性
仮想DOMは破壊的に上書きせず、毎回レンダリングされるたび、新しい仮想DOMが生成されます。
軽量性
DOMだと履歴として保持するには、ブラウザの負担が重すぎます。しかし、仮想DOMは軽く、メモリ使用量が少なく済みます。
参照保持
従来のDOMでの操作は破壊的メソッドによって変更していました。
const history = [];
let currentState = ['買い物', '掃除'];
// 履歴に保存
history.push(currentState);
// 破壊的更新
currentState.push('洗濯');
// 問題:履歴も一緒に変わってしまう
console.log(history[0]); // ['買い物', '掃除', '洗濯'] ← 履歴が汚染
仮想DOMは破壊的メソッドではなく、新しい配列を作成して更新します。
Reactでスプレッド構文をよく使用するのはこのためです。
新しいオブジェクトを作成することで、参照が変わり、差分が検知しやすくなります。
// ❌ 破壊的更新(Reactでは推奨されない)
const [items, setItems] = useState(['りんご', 'バナナ']);
const addItem = () => {
items.push('オレンジ'); // 元の配列を変更
setItems(items); // 参照が同じなので変更が検知されない
};
// ✅ 非破壊的更新(Reactの推奨方法)
const addItem = () => {
setItems([...items, 'オレンジ']); // 新しい配列を作成
// 参照が変わるので変更が確実に検知される
};
構造化
従来のDOMは値の変化しか記録することができませんでした。仮想DOMは、いつ、何が、なぜ、何から何に変わったかなどの詳細な情報を記録することができます。
これらの理由により、仮想DOMは履歴管理を行うことができます。
仮想DOMがあることでよいこと
1. パフォーマンスの向上
仮想DOMはJavaScriptオブジェクトです。
実際のDOM操作という重い処理を最小限に抑えることで、動的な変更が高速になりました。
2. コンポーネント化のしやすさで再利用性、保守性の向上
ReactやVue.jsが登場する前のjQuery全盛期でもコンポーネント化の概念はありました。
しかし、jQueryは手動でDOMを監視・更新する必要があり複雑でした。
仮想DOMを使うと状態変更時に自動で再描画されることで、管理がシンプルになります。
コンポーネント化により、以前は長いコードで読むのが大変でしたが、短いコンポーネントを組み合わせることで理解しやすくなりました。また再利用性も高くなり、同じコードを何度も書くことが少なくなります。
3. 宣言的プログラミングでバグの減少。可読性の向上
従来のDOM操作は「どうやって」変更するかをすべて細かく指定する命令的プログラミングでした。
一方、仮想DOMを使った宣言的プログラミングでは「何を表示したいか」を宣言するだけです。
これにより可読性が格段に向上し、バグの発生を大幅に減らすことができます。
また、コードを読むだけで「どんなことをしたいのか」が直感的に理解できるようになります。
// 「どうやって」変更するかを細かく指定
function toggleTodo(id) {
const item = document.getElementById(`todo-${id}`);
if (item.classList.contains('completed')) {
item.classList.remove('completed');
item.querySelector('.checkbox').checked = false;
item.querySelector('.text').style.textDecoration = 'none';
} else {
item.classList.add('completed');
item.querySelector('.checkbox').checked = true;
item.querySelector('.text').style.textDecoration = 'line-through';
}
}
// 「何を表示したいか」だけを宣言
function TodoItem({ todo, onToggle }) {
return (
<div className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
</div>
);
}
おわりに
Reactを学習していて、仮想DOMとはなんなのだろうかと気になりまとめてみました。
学習の手助けになれれば幸いです。
ありがとうございました。