Web開発の勉強を始めると、絶対に出会う「DOM(ドム)」とか「DOM操作」っていう言葉。
「なんとなくわかるけど、人には説明できない…」
「HTMLと何が違うの?」
そんな「DOMってナンダと思っている」のあなた!
DOMの正体を「家づくり🏠」に例えながら、Webページが動く仕組みから、ReactやVueが使う「仮想DOM」まで、分かりやすく解説します!
DOMの定義
DOMを調べると、だいたい2つの説明が出てきて混乱しませんか?
- 「Webページの要素をツリー構造で表現したデータモデルのこと」
- 「HTMLをJavaScriptから触れるようにする API(インターフェース) のこと」
「モデル? API? どっちなんだよ!」
これは、DOMはその両方の意味を持っているためにこの混乱が起きていると思います。
この「2つの顔」を理解するために、Webページが表示されるまでを「家づくり」に例えてみましょう。
-
HTMLファイル 📄:
あなたが書くコード。これは「静的な設計図」です。ただのテキストファイルなので、これだけでは動きません。 -
ブラウザ (Chromeなど) 👷:
設計図を読んで、実際に家を建てる「建設業者」です。 -
JavaScript 🎨:
完成した家を見て、壁紙を変えたり、家具を動かしたりする「インテリアデザイナー」です。
さて、ここで問題です。デザイナー(JavaScript)は、どうやって家(Webページ)をリフォームするんでしょうか?
設計図(HTMLファイル)を直接書き換える? …いいえ、違います。
建設業者(ブラウザ)は、設計図(HTML)を読み込むと、それを解釈して、ブラウザのメモリ上(頭の中)に「操作可能な、本物の建物(あるいは超精巧な3Dモデル)」を構築します。
この「ブラウザが構築した、操作可能な建物モデル」こそが、
①「モデルとしてのDOM」
です。
そして、デザイナー(JavaScript)がその建物モデルを安全に操作(リフォーム)するために、建設業者(ブラウザ)が用意した「公式の道具箱(建物の鍵、ハンマー、ペンキ、指示書)」。これが
②「APIとしてのDOM」 です。
DOMとは、この「建物モデル(モノ)」と「操作用の道具箱(ルール)」の、切り離せない一体の概念です。
DOMの本質①:ブラウザが作る「生きた設計図(モデル)」
まずは「モデルとしてのDOM」から見ていきましょう。
建設業者(ブラウザ)は、私たちが書いたHTML(設計図)を上から順番に読み込み、「DOMツリー」と呼ばれる階層構造(家系図みたいなもの)をメモリ上に作ります。
「ノード」ってなに?
このDOMツリーは、「ノード(Node)」という部品の集まりでできています。
家が「土地」「部屋」「壁」「家具」といった部品(ノード)でできているのと同じです。
主なノードはこんな感じです。
- Documentノード: ツリー全体のてっぺん。いわば「土地」そのもの。
-
要素ノード (Element Node):
<html>や<body>、<h1>、<p>などのタグ。いわば「部屋」や「柱」といった建物の構造体です。 - テキストノード (Text Node): 「こんにちは!」のような、要素に含まれる文字。いわば「壁紙のデザイン」や「家具」です。
DOMツリーを覗いてみよう
例えば、こんなシンプルなHTML(設計図)があるとします。
<!DOCTYPE html>
<html>
<head>
<title>DOMの世界</title>
</head>
<body>
<h1>DOMは友達</h1>
<p>怖くないよ!</p>
</body>
</html>
建設業者(ブラウザ)は、これを読み込んで、メモリ内に次のようなDOMツリー(生きた建物モデル)を構築します。
この「親子関係」(例: <body> の子は <h1> と <p>)や「兄弟関係」(例: <h1> と <p> は兄弟)が、めちゃくちゃ重要です。
なぜなら、後で説明するAPI(道具箱)は、すべてこの「関係性」を前提に作られているから。「<body> の子として新しい部屋(<div>)を追加する」みたいに使うんです。
DOMの本質②:ページを改造する「魔法の道具箱(API)」
さて、「生きた建物(モデル)」が完成しました。いよいよ、デザイナー(JavaScript)がリフォーム(DOM操作)を開始します。
そのために使うのが「APIとしてのDOM」=「魔法の道具箱」です。
「DOM操作」ってなに?
DOM操作とは、一言でいえば「JavaScriptがDOM API(道具)を使って、DOMツリー(建物)を変更・追加・削除すること」です。
具体的には、以下のようなリフォーム作業がすべて「DOM操作」にあたります。
- 要素の取得: 「id="main" の部屋を探す」
-
要素の作成: 「新しい
<p>(部屋)を作る」 -
要素の追加・削除: 「
<body>(建物)に新しい部屋(<p>)を増築する」 -
内容の変更: 「
<h1>(部屋)の壁紙(テキスト)を書き換える」 -
スタイルの変更: 「
<p>(部屋)の色を赤く塗る」
よく使う「道具(API)」の紹介 🛠️
道具箱(DOM API)には、たくさんの道具(メソッド)が入っています。
-
document.getElementById("id名")- 建物の住所録
- 指定されたID(住所)を持つ要素(部屋)を、ピンポイントで見つけます。
-
document.querySelector("セレクタ")- 高度な探知機
- CSSセレクタ(例:
.classnameやdiv > p)に一致する、最初の要素を見つけます。
-
document.createElement("タグ名")- 3Dプリンタ
- 指定されたタグ名(例:
li)の要素ノード(部屋の部品)を、新しく作ります。
-
element.appendChild(子要素)- クレーン
- 見つけた要素(element)の中に、作った部品(子要素)を「最後の子」として設置(増築)します。
-
element.textContent = "テキスト"- 魔法のペンキ
- 要素(部屋)の中身を、指定したテキスト(壁紙)で丸ごと塗り替えます。
実践!DOM操作デモ
最もよくある「ボタンを押すとリストに項目が追加される」処理を見てみましょう。
HTML(設計図)
<ul id="itemList">
<li>最初のアイテム</li>
</ul>
<button id="addButton">追加</button>
JavaScript(デザイナーのリフォーム手順書)
// デザイナー(JavaScript)のリフォーム手順書
// 0. DOM構築完了を待つ (※超重要!次のセクションで解説)
window.addEventListener('DOMContentLoaded', () => {
// 1. 必要な場所と道具を準備 (APIで要素ノードを取得)
// 「itemList」という住所のリスト(ul)と、
// 「addButton」という住所のボタン(button)を探す
const list = document.getElementById('itemList');
const button = document.getElementById('addButton');
// 2. ボタンに「クリックされたら」という指示を出す (APIでイベント登録)
button.addEventListener('click', () => {
// --- クリックされた瞬間に、以下のリフォームを実行 ---
// 3. 新しい <li> (部屋の部品) を作成 (API: createElement)
const newItem = document.createElement('li');
// 4. 新しい <li> の中身 (壁紙) を設定 (API: textContent)
const newText = '新しいアイテム ' + (list.children.length + 1);
newItem.textContent = newText;
// 5. <ul> (リスト本体) に新しい <li> (部品) を追加 (API: appendChild)
list.appendChild(newItem);
// これが「DOM操作」の正体です!
});
});
DOM操作の「タイミング」と「落とし穴」
さっきのデモコードで、window.addEventListener('DOMContentLoaded', ...) というJavaScriptが出てきましたね。
これは、DOM操作における 最大の「落とし穴」 を回避するためのものです。DOM操作は「何を」やるかと同じくらい、「いつ」やるかが重要です。
落とし穴①:建設完了前にリフォームしようとする 🚧
HTMLはブラウザによって上から下に読み込まれます。もし、JavaScriptを<head>タグの中に書いて、document.getElementById('itemList') を実行しようとしたらどうなるでしょう?
<head>
<script>
// デザイナー(JavaScript)のリフォーム手順書
// 0. DOM構築完了を待つ (※超重要!次のセクションで解説)
// これをコメントアウトすることでDOMツリー構築を待たずにDOM操作を実行してみる
// window.addEventListener('DOMContentLoaded', () => {
// 1. 必要な場所と道具を準備 (APIで要素ノードを取得)
// 「itemList」という住所のリスト(ul)と、
// 「addButton」という住所のボタン(button)を探す
const list = document.getElementById('itemList');
const button = document.getElementById('addButton');
// 2. ボタンに「クリックされたら」という指示を出す (APIでイベント登録)
button.addEventListener('click', () => {
// --- クリックされた瞬間に、以下のリフォームを実行 ---
// 3. 新しい <li> (部屋の部品) を作成 (API: createElement)
const newItem = document.createElement('li');
// 4. 新しい <li> の中身 (壁紙) を設定 (API: textContent)
const newText = '新しいアイテム ' + (list.children.length + 1);
newItem.textContent = newText;
// 5. <ul> (リスト本体) に新しい <li> (部品) を追加 (API: appendChild)
list.appendChild(newItem);
// これが「DOM操作」の正体です!
});
// });
</script>
</head>
<ul id="itemList">
<li>最初のアイテム</li>
</ul>
<button id="addButton">追加</button>
なぜなら、その時点では建設業者(ブラウザ)はまだ<body>(建物の本体)を読み始めてすらいません。つまり、建設中の現場にデザイナーが乗り込んできて、「まだ存在しない部屋」を探しているのと同じ状況です。
解決策:「建設完了」の合図を待つ
リフォーム(DOM操作)を開始するタイミングは、主に2つあります。
-
DOMContentLoaded
- 合図: 「建物の 構造(DOMツリー) が完成したら、すぐにリフォーム開始!」
- 画像やCSS(家具やペンキ)の搬入は待ちません。
- これが最も一般的で高速なベストプラクティスです。 (さっきのデモで使ったやつ)
-
load
- 合図: 「建物の構造が完成し、 すべての家具・ペンキ(画像、CSS) が搬入・乾燥し終わったら、リフォーム開始!」
- ページの見た目がすべて整ってからでないと実行できない処理(例: 画像のサイズを取得する)に使います。
落とし穴②:後から増築した部屋のイベントが効かない
もう一つの「落とし穴」です。
さっきのデモでは、最初から存在していた「追加ボタン」にクリック処理を登録しました。
では、「リストに追加した <li> をクリックしたら削除する」処理を追加したい場合、どう書けばよいでしょうか?
// 悪い例:これは動かない
const listItems = document.querySelectorAll('#itemList li');
listItems.forEach(item => {
item.addEventListener('click', () => {
item.remove(); // 削除処理
});
});
このコードは、最初からHTMLに書いてあった <li> にしか効きません。
addEventListener(指示書)は、その瞬間に存在していた部屋(DOMノード)にしか配れないからです。
ボタンを押して新しく増築した <li>(動的要素)には、指示書が配られていないため、クリックしても何も起こりません。
解決策:「イベント委譲(Event Delegation)」
この問題を解決するのが「イベント委譲」というテクニックです。
これは、「個別の部屋(<li>)にいちいち警備員(リスナー)を配置するのではなく、建物全体(親要素の <ul>)の入り口に、一人の優秀な警備員を配置する」という戦略です。
// 良い例:イベント委譲
// 親要素 (ul#itemList) に警備員(リスナー)を一人だけ配置
const list = document.getElementById('itemList');
list.addEventListener('click', (event) => {
// 建物(ul)の中でクリックイベントが発生!
// 警備員が「どこで」発生したかチェック
// event.target は「クリックされた実際の要素」を指す
// もしクリックされたのが <li> (またはその中身) だったら
if (event.target.closest('li')) {
// 処理を実行!
console.log(event.target.textContent + ' がクリックされました');
// これなら、後から増築(追加)された <li> でもちゃんと動作する
event.target.closest('li').remove(); // クリックされたliを削除
}
});
この警備員(リスナー)は、<ul> の中で発生したすべてのクリックを監視します。そして、クリックされた場所(event.target)が <li> であれば、処理を実行します。
これなら、後からいくら <li> を増築しても、入り口の警備員がすべて検知してくれるため、問題なく動作します。
🚀 なぜ現代は「仮想DOM」の考え方を使用するのか?
DOM操作は強力ですが、大きな「代償」があります。
それは、**DOM操作は、実はものすごく「重い(=遅い)」**作業だということです。
DOM操作の「重い代償」
なぜ重いのか?
DOM(生きた建物)を少しでも変更すると、建設業者(ブラウザ)は即座に「再計算」の連鎖を始めるからです。
-
リフロー(Reflow): ある要素のサイズが変わると、「待って、それじゃあ他の要素の位置も全部ズレるじゃないか!」と、ページ全体のレイアウトを再計算します。
- 1階の柱を1本動かしただけで、2階、3階...建物全体の強度計算と間取りの再計算が走る)
-
リペイント(Repaint): 再計算されたレイアウトを、画面に「再描画」します。
- 再計算した結果、建物の見た目をすべて描き直す*)
昔のWebサイトならこれでも問題ありませんでした。しかし、現代のSNSのタイムラインのように、1秒間に何十回もDOMが書き換わる複雑なアプリでは、この「重いリフォーム」が頻発し、ページがカクカク(パフォーマンス低下)になってしまいます。
仮想DOM(Virtual DOM)の登場
この問題を解決するために、ReactやVue.jsといった現代のライブラリは、「仮想DOM(Virtual DOM)」という賢い戦略を採用しました。
仮想DOMとは?
本物のDOM(生きた建物)の「JavaScriptオブジェクト(ただのデータ)で作った、超軽量なコピー(設計図のコピー、またはミニチュア模型)」です。
この仮想DOMは、メモリ上に存在する「ただのデータ」なので、これを1秒間に1万回書き換えても、ブラウザの「重い」リフローやリペイントは一切発生しません。
Reactコードで見る仮想DOM
例えば、こんなReactコンポーネント(JSXコード)があるとします。
import { useState } from 'react';
function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? 'いいね!済み' : 'いいね!'}
</button>
);
}
JSXとは?
JSX とは JavaScript の拡張であり、JavaScript ファイル内に HTML のようなマークアップを書けるようにするものです。
https://ja.react.dev/learn/writing-markup-with-jsx
Reactは、このJSXコードを仮想DOM(JavaScriptオブジェクト)に変換します。実際には、こんな感じのデータ構造です:
// 仮想DOMのイメージ(実際はもっと複雑)
{
type: 'button',
props: {
onClick: () => {...},
children: 'いいね!'
}
}
この変換プロセスを図で見ると、こうなります:
従来のDOM操作なら、こう書く必要がありました:
// 従来のDOM操作(重い!)
const button = document.querySelector('button');
button.addEventListener('click', () => {
if (button.textContent === 'いいね!') {
button.textContent = 'いいね!済み';
} else {
button.textContent = 'いいね!';
}
// この時点で、ブラウザは即座にリフロー・リペイントを実行
});
Reactの仮想DOMなら、setLiked(!liked) を呼ぶだけで、仮想DOM(軽量なデータ)だけが更新され、実際のDOMは後で効率的に更新されます。
「props」ってなに?
props(プロップス)は、Reactにおいて「要素に渡される属性やデータ」を表すオブジェクトです。
-
DOMの世界: HTMLの
<button class="btn" id="myBtn">では、classやidが属性(attributes) -
Reactの世界:
<button className="btn" id="myBtn">では、classNameやidがpropsとして扱われる
propsの主な特徴:
-
要素の属性:
className、id、styleなど、HTMLの属性に対応 -
イベントハンドラ:
onClick、onChangeなど、ユーザーの操作を処理する関数 -
子要素:
childrenとして、要素の中身(テキストや他の要素)が入る
// JSXでの記述
<button className="like-btn" onClick={handleClick}>
いいね!
</button>
// 実際の仮想DOM(propsとして保存される)
{
type: 'button',
props: {
className: 'like-btn', // HTMLのclass属性
onClick: handleClick, // クリック時の処理
children: 'いいね!' // ボタンのテキスト
}
}
このpropsオブジェクトを使うことで、Reactは「どの要素が、どんな属性を持っているか」「どんなイベントが登録されているか」を記憶し、効率的にDOMを更新できるのです。
propsの構造を図で見ると、こうなります:
従来のDOM操作とReactの仮想DOMの違いを図で見てみましょう:
仮想DOMの賢いリフォーム術
ReactやVueは、この仮想DOM(模型)を使って、リフォームを最小限に抑える「賢い現場監督」のように振る舞います。
- 状態変更: アプリの「状態(State)」が変わる(例: ユーザーが「いいね!」を押す)。
- 仮想DOMの更新: 現場監督(React)は、まず「本物の建物(DOM)」には一切触れません。代わりに、事務所で「模型(仮想DOM)」だけを高速に書き換えます。
- 差分検出(Diffing): 監督は「古い模型(変更前の仮想DOM)」と「新しい模型(変更後の仮想DOM)」を並べて、差分を比較します。
- 最小限の変更を特定: 「ああ、変更点はたった一箇所、3階の部屋の『いいね!ボタン』の色だけだな」と、本物のDOMを変更すべき最小限の差分を特定します。
- 一括更新(Reconciliation): 監督は、本物のDOM(建設業者)に対し、この時初めて、たった一度だけ指示を出します。「3階のボタンの色だけ、これに変更してくれ。他は一切触るな」と。
Reactコードで見る差分検出の仕組み
先ほどのLikeButtonコンポーネントで、ユーザーが「いいね!」ボタンをクリックした時の流れを見てみましょう。
変更前の仮想DOM
// liked = false の状態
{
type: 'button',
props: { children: 'いいね!' }
}
変更後の仮想DOM
// liked = true の状態
{
type: 'button',
props: { children: 'いいね!済み' }
}
Reactは、この2つを比較して、「button要素のchildrenプロパティだけが変わった」ことを検出します。そして、実際のDOMでは以下のように、ボタンのテキスト部分だけを効率的に更新します:
// Reactが実際に行う操作(最小限!)
button.textContent = 'いいね!済み';
// 他の要素(親のdiv、他のコンポーネントなど)には一切触らない
この差分検出のプロセスを図で見ると、こうなります:
もしリスト全体を再描画していたら?
// 非効率な例:全て再描画(Reactはこうしない)
ul.innerHTML = ''; // 一旦全部削除
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
ul.appendChild(li); // 全部作り直し(重い!)
});
Reactの仮想DOMなら、変更された要素だけをピンポイントで更新するため、パフォーマンスが大幅に向上します。
このフローを図解すると、こうなります。
仮想DOMは、本物のDOM(重いリフォーム)に触れる回数を「必要最小限」に抑えるための、非常に賢い戦略なのです。これにより、現代の複雑なアプリケーションでもヌルヌル動くパフォーマンスを維持できます。
最後に、DOMの本質をもう一度まとめます。
DOMは、単なるHTMLファイルでも、JavaScriptの一部でもありません。
DOMとは、静的な「設計図(HTML)」と、動的な「デザイナー(JavaScript)」の間を繋ぐ、ブラウザが提供する 「橋渡し役」であり「通訳」 です。
- DOMは、ブラウザがメモリ上に構築する 「生きたモデル(建物)」 であると同時に、
- それを操作するための唯一の 「公式ツール(API)」 でもあります。
DOMを学ぶことは、Web開発の「魂」を理解すること。
この記事が、あなたの曖昧だった理解を「自分の言葉で説明できる」確かな知識に変える一助となれば幸いです!


