こんにちは、かつコーチです。
前回はコンポーネントの作り方を解説しました。
今回は少し視点を変えて、「Reactはなぜサクサク動くのか」という仕組みの話をしていきます。
キーワードは仮想DOMです。
やや中級者向けの内容にはなりますが、この後Propsやstateを学んでいくうえで理解しておくと、Reactの動きが腹落ちしやすくなるテーマです。
そもそもDOMとは?
ブラウザが画面を描画する仕組み
仮想DOMの話をする前に、まず「DOM(Document Object Model)」について押さえておきましょう。
DOMとは、HTMLの構造をブラウザが理解できる「木構造のデータ」として表現したものです。
JavaScriptでdocument.getElementByIdのような操作をすると、このDOMを直接書き換えて画面の見た目を変えることができます。
// 素のJavaScriptでDOMを直接操作する例
const heading = document.getElementById("title");
if (heading) {
heading.textContent = "更新後のタイトル";
}
一見便利な仕組みですが、実はこのDOM操作には大きなコストがかかります。
DOM操作が「重い」理由
DOMを1箇所書き換えるだけでも、ブラウザの内部では以下のような処理が走ります。
- 要素のスタイル・レイアウトの再計算
- 画面への再描画(再レンダリング)
この処理は見た目以上に負荷が高く、DOM操作の回数が増えるほどアプリの動きが重くなっていきます。
昔ながらのJavaScriptでリストの表示を頻繁に更新するようなアプリを作ると、この「DOM操作のコスト」がボトルネックになりやすいという課題がありました。
仮想DOMとは?
「仮想」のDOMをメモリ上に持つ仕組み
Reactが採用しているのが仮想DOM(Virtual DOM)という仕組みです。
仮想DOMとは、実際のDOMとは別に、メモリ上に作られる「DOMの写し」のことです。
Reactは、画面の状態が変化するたびに、いきなり本物のDOMを書き換えるのではなく、まず仮想DOM上で「次はこういう見た目になるはずだ」という新しい状態を作ります。
差分だけを見つけて反映する「差分検出」
ここからがポイントです。
Reactは、更新前の仮想DOMと更新後の仮想DOMを比較して、「実際に変わった部分」だけを見つけ出します。
この処理を差分検出(Diffing)と呼びます。
そして、見つけた差分だけを本物のDOMに反映します。
// カウンターの数字だけが変わる例
function Counter({ count }: { count: number }) {
return (
<div>
<h1>カウンターアプリ</h1>
<p>現在の値:{count}</p>
</div>
);
}
このコンポーネントでcountの値だけが変わった場合、Reactは「<p>タグの中の数字部分だけ」を検出し、<h1>や周辺の<div>はそのままに、変わった箇所だけをピンポイントで書き換えます。
画面全体を毎回作り直すのではなく、「本当に変わったところだけ」を効率よく更新する——これが、Reactが軽快に動く理由です。
仮想DOMのメリットを整理する
開発者が意識しなくていい「最適化」
仮想DOMの一番のメリットは、開発者が「どこを更新すべきか」を自分で考えなくていいという点です。
素のJavaScriptでは、「この部分だけ書き換える」という処理を自分で細かく書く必要がありました。
Reactでは、「今の状態はこうです」という宣言的な書き方をするだけで、実際にどこをどう更新するかはReact側の差分検出が引き受けてくれます。
// 「どう更新するか」ではなく「今どんな状態か」を書くだけでいい
function App() {
const items: string[] = ["りんご", "みかん", "ぶどう"];
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
itemsが増えても減っても、Reactが差分を見つけて必要な<li>だけを追加・削除してくれます。
この「宣言的に書けばあとは任せられる」という感覚こそが、Reactを使う一番のメリットだと私は感じています。
仮想DOMは「万能」ではない
私がつまずいた誤解
ここで、私が実際につまずいた経験を1つ共有します。
Reactを学び始めたばかりの頃、「仮想DOMを使っているから、Reactは何でも自動的に高速になる」と思い込んでいた時期がありました。
ところが、大量のデータを一覧表示するページで、明らかに画面がカクつく場面に遭遇しました。
原因を調べてみると、リストのkey(Reactが要素を識別するための目印)に、配列のインデックス番号をそのまま使っていたことが分かりました。
❌ Before:keyに配列のインデックスをそのまま使う
function ItemList({ items }: { items: string[] }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
リストの並び替えや途中への追加・削除が発生すると、インデックスがずれてしまい、Reactが「どの要素が本当に変わったのか」を正しく判断できなくなります。
その結果、本来更新しなくていい要素まで再描画されてしまい、差分検出のメリットが十分に活かせていませんでした。
✅ After:データ固有のIDをkeyに使う
type Item = {
id: string;
name: string;
};
function ItemList({ items }: { items: Item[] }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
idのような、要素ごとに一意で変化しない値をkeyに使うことで、Reactは要素の同一性を正確に追跡できるようになります。
このとき初めて、「仮想DOMは万能の最適化装置ではなく、正しく使って初めて効果を発揮する仕組みなんだ」と実感しました。
keyの重要性については、リストの表示を扱う後の記事でも改めて触れていきます。
まとめ
この記事のポイント
- 本物のDOMを直接操作する処理は負荷が高く、更新回数が増えるとアプリが重くなりやすい
- 仮想DOMは、メモリ上に持つ「DOMの写し」を使って更新前後の差分だけを検出する仕組み
- 開発者は「今の状態」を宣言的に書くだけで、実際の更新処理はReactの差分検出に任せられる
- リストの
keyにインデックスを使うと差分検出が正しく働かず、パフォーマンスが悪化することがある
次に読むべき記事
仮想DOMの仕組みが分かったところで、次回はいよいよコンポーネント間で値をやり取りする「Props」を解説します。
→ 次の記事:Propsでコンポーネントに値を渡す方法