Virtual DOM — メモリ上のUIツリー

ReactがUIライブラリとして成功した最大の要因のひとつがVirtual DOMです。 Virtual DOMとは、実際のDOM(Document Object Model)のメモリ上の軽量コピーであり、 UIの「理想的な状態」をJavaScriptオブジェクトとして表現したものです。

ブラウザのDOMは非常にコストの高いAPIです。ノードの生成・属性変更・レイアウト再計算は すべてメインスレッドをブロックします。Reactはこの問題に対し、「まずメモリ上で差分を計算し、必要最小限の変更だけを実DOMに反映する」というアプローチを取りました。

Reconciliation — 差分検出と同期

ReactがVirtual DOMの新旧ツリーを比較し、実DOMへの最小限の更新を計算するプロセスをReconciliation(差分調停)と呼びます。 このアルゴリズムがReactの心臓部です。

一般的なツリーの差分アルゴリズムはO(n³)の計算量を持ちます。 1000個のノードを持つツリーでは10億回の比較が必要になり、UIの更新には到底使えません。 Reactはこれを2つのヒューリスティクスによってO(n)まで削減しました。

2つのヒューリスティクス

  1. 異なる型の要素は異なるツリーを生成する<div><span>に変わった場合、 Reactは古いサブツリー全体を破棄し、新しいツリーをゼロから構築します。 子孫要素の差分を調べるコストを完全にスキップできます。
  2. key属性で要素の同一性を追跡する — リスト内の要素が並べ替えられた場合、key属性により どの要素が移動・追加・削除されたかを正確に特定します。keyがなければ、Reactは全アイテムを再レンダリングしてしまいます。
graph TD
  A["状態更新発生"] --> B["新しいVirtual DOMツリーを生成"]
  B --> C["旧ツリーと新ツリーをDiff"]
  C --> D{"要素の型が同じか?"}
  D -->|同じ| E["属性のみ更新"]
  D -->|異なる| F["サブツリー全体を再構築"]
  E --> G["子要素を再帰的に比較"]
  G --> H["最小限のDOM操作を生成"]
  F --> H
  H --> I["実DOMに反映"]
Reconciliationの処理フロー

React Fiber — Reconcilerの全面刷新

React 16(2017年)で導入されたFiberは、 Reconcilerを根本から書き直した新しいアーキテクチャです。 旧Reconciler(Stack Reconciler)は同期的にツリー全体を処理するため、 大規模なUIの更新でメインスレッドが長時間ブロックされ、 アニメーションのカクつきや入力の遅延を引き起こしていました。

Fiberの設計思想は「React専用の仮想スタックフレーム」です。 JavaScriptのコールスタックは一度開始すると途中で中断できませんが、 Fiberは処理単位を細かく分割し、ブラウザに制御を返しながら段階的にレンダリングを進めます。

Fiber Nodeの構造

Fiberツリーの各ノード(Fiber Node)は、コンポーネントやDOM要素1つに対応する JavaScriptオブジェクトです。以下の主要プロパティを持ちます:

プロパティ役割説明
type要素の種類"div"MyComponentなど
key同一性識別子Reconciliationでの要素追跡に使用
child最初の子ノード子要素の先頭へのポインタ
sibling次の兄弟ノード同じ親の次の要素へのポインタ
return親ノード処理完了後に戻る先
pendingProps新しいProps今回のレンダリングで適用されるProps
memoizedStateHooks状態useState等の状態が格納されるリンクドリスト
alternate対のFiberダブルバッファリングの相手方

Fiber Nodeはリンクドリスト構造で接続されています。 従来の再帰的なツリー走査ではなく、childsiblingreturnのポインタをたどるループ処理でツリーを走査します。 これにより、任意のノードで処理を中断し、後から再開することが可能になりました。

graph TD
  App["App"] -->|child| Header["Header"]
  Header -->|sibling| Main["Main"]
  Main -->|sibling| Footer["Footer"]
  Header -->|return| App
  Main -->|return| App
  Footer -->|return| App
  Main -->|child| Article["Article"]
  Article -->|sibling| Sidebar["Sidebar"]
  Article -->|return| Main
  Sidebar -->|return| Main
Fiber Nodeのリンクドリスト構造(child / sibling / return)

2フェーズモデル — Render と Commit

Fiberアーキテクチャの核心は、レンダリングを2つのフェーズに明確に分離したことです。

特性RenderフェーズCommitフェーズ
中断可能(優先度の高いタスクに譲る)不可(一気に完了する)
副作用なし(純粋な計算のみ)あり(DOM操作、Effect実行)
処理内容Fiber Nodeの生成・更新・Diff計算実DOMへの反映、ref設定、ライフサイクル呼び出し
実行環境メモリ上(ユーザーに不可視)ブラウザDOM上(ユーザーに可視)
繰り返し可能(中断後に最初からやり直し得る)1回のみ
graph LR
  A["状態更新"] --> B["Render Phase"]
  B -->|"中断可能 / Work Loop"| C{"全Fiberの\n処理完了?"}
  C -->|No| D["ブラウザに制御を返す"]
  D -->|"次のフレームで再開"| B
  C -->|Yes| E["Commit Phase"]
  E -->|"中断不可"| F["DOM更新"]
  F --> G["useEffect実行"]
  G --> H["完了"]
Fiberの2フェーズモデルとWork Loop

優先度制御とスケジューリング

FiberはPullベースのスケジューリングを採用しています。 フレームワーク側が更新のタイミングと優先度を制御し、 最適なタイミングでレンダリングを実行します。

内部的には、更新に優先度(Lane)が割り当てられます。 ユーザー入力(クリック、タイピング)は最高優先度で即座に処理される一方、 データフェッチ結果の反映などは低優先度として遅延実行されます。 これがReact 18以降のConcurrent Renderingの基盤となっています。

JSXの仕組み — シンタックスシュガーの裏側

JSXはReactで最も目にする構文ですが、その正体はReact.createElement()のシンタックスシュガーです。 ブラウザもNode.jsもJSXを直接実行できません。 BabelやTypeScriptなどのトランスパイラが、ビルド時にJSXを通常のJavaScript関数呼び出しに変換します。

旧方式 — Classic JSX Transform

React 17以前の方式では、JSXは以下のように変換されていました:

// JSX
<h1 className="title">Hello, React!</h1>

// トランスパイル後
React.createElement('h1', { className: 'title' }, 'Hello, React!')

この方式では、JSXを使うすべてのファイルでimport React from 'react'が必要でした。React.createElementを呼ぶためにReactがスコープに存在する必要があったためです。

新方式 — Automatic JSX Runtime

React 17で導入された新しいJSXトランスフォーム(Automatic Runtime)では、 トランスパイラが自動的にランタイムをインポートします:

// JSX(React 17+)
<h1 className="title">Hello, React!</h1>

// トランスパイル後
import { jsx as _jsx } from 'react/jsx-runtime';
_jsx('h1', { className: 'title', children: 'Hello, React!' })

この変更により、Reactのインポートが不要になりました。 バンドルサイズのわずかな削減に加え、 将来的なJSXランタイムの変更を容易にする設計上のメリットもあります。

Virtual DOMのトレードオフ

Virtual DOMは強力な抽象化ですが、万能ではありません。 そのアプローチには固有のオーバーヘッドがあります。

構造的なオーバーヘッド

Reactはコンポーネントのどこが変更されたかを正確に知りません。 状態が更新されると、そのコンポーネントとすべての子孫コンポーネントを再レンダリングし、 Virtual DOM上でDiff計算を行います。 たとえ結果が「変更なし」であっても、この計算コストは発生します。

対照的に、SvelteやSolidが採用するSignal方式は、 変更された値を使用しているUI部分を直接特定し、ピンポイントで更新します。 Virtual DOMを経由しないため、理論上のオーバーヘッドはゼロに近づきます。

比較項目Virtual DOM(React)Signal(Svelte / Solid)
変更追跡コンポーネント単位値単位(リアクティブ変数)
更新粒度サブツリー全体を再計算依存するDOM箇所のみ更新
中間表現Virtual DOMツリーが必要不要(直接DOM操作)
INPベンチマーク~68ms(React 19)~24ms(Svelte 5)
出典: Krausest JS Framework Benchmark
メモリ使用量2つのFiberツリーを保持軽量なリアクティブグラフ

React Compilerによる対策

ReactチームはこのオーバーヘッドをReact Compiler(旧React Forget)で構造的に解決しようとしています。 React Compilerはビルド時にコードを解析し、自動的にメモ化(useMemoやuseCallback相当の最適化)を挿入します。 開発者が手動でメモ化を管理する必要がなくなり、 Virtual DOMのDiff計算で「変更なし」と判定されるケースを大幅に削減します。

理解度チェック

問題 0 / 40%
Q1

Reactが一般的なツリー差分アルゴリズムのO(n³)をO(n)に削減するために使用しているヒューリスティクスはどれですか?

キーボード: 1〜4 で選択、Enter で回答