Reactのレンダリングフローを理解する:JSX、Fiber、リアルDOM
執筆理由
JSXやレンダリングプロセスを理解すると開発効率が大きく向上すると思っています。
特に、宣言的UI(UIがどうあるべきかを記述するスタイル)や仮想DOMといった設計思想を理解することで、再レンダリングや最適化の判断がしやすくなり、より安定した開発が可能になります。
Reactに触れる前にReactのレンダリングフローを確認するようにしているのですが、都度Web上に落ちてる情報を集めていて時間がかかりすぎていました...。
そこで今回、React JSXとレンダリングプロセスをステップごと(〜6)に整理して記事にしてみました。
何かの名称の由来についても認識しておくと本質的な理解につながると思います。
そもそもJSXとは?
ReactのJSX(JavaScript XML)は、HTMLのように記述できる構文拡張です。Reactで使用され、コンポーネントのUIを宣言的に記述するために利用されます。
JSXを使うことで、UIの構造を直感的に記述できるため、可読性が向上します。
コーダー出身の方ならとっつきやすいはず…。
こんなやつです。
const App = () => {
return (
<div>
<h1>App</h1>
</div>
)
}
今回使うコード
viteは使わず、npx create-react-appでセットアップしています。
JSXとレンダリングフローを確認するのが目的なのでコンポーネントなどindex.jsxにひとまとめにしています。cssなど他のファイルは変更していません。
下部のsetTimeoutに渡しているコールバックはFiberノードの確認用のコードです。
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import './App.css';
const App = () => {
const Child1 = () => {
return (
<div>
<h2>Child1</h2>
</div>
)
}
const Child2 = () => {
return(
<div>
<h2>Child2</h2>
</div>
)
}
return (
<div>
<h1>App</h1>
<Child1 />
<Child2 />
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// Fiberノードの確認
setTimeout(() => {
const rootFiber = root._internalRoot.current;
console.log(rootFiber);
}, 500);
レンダリングのフローを見て行きます。
Step1 - JSXの状態(開発中)
Reactは、JSXで記述された関数を1つのUI部品(Reactコンポーネント)として扱い、それらを組み合わせて仮想的な構造(コンポーネントツリー)を作成します。

- コンポーネントツリー
- あくまで概念的な構造であり、実体ではありません(この後、JSX → 仮想DOMのフローで実体化)。
- Reactコンポーネント
- JSXで記述した関数は一つのReactコンポーネントとしてみなされます。UIの再利用可能なパーツとして機能します。
- React要素
- Reactコンポーネントがreturnする静的な要素(div、h1など…)を指します。React.createElement()に変換後、そのデータを元に仮想DOMが生成されます。
Step2 - JSXのビルド処理
JSXはそのままではブラウザが解釈できないため、ビルドの過程で、Babel(webpack版)やesbuild(vite版)によって React.createElementの形式にトランスパイルされます。さらに、その後のステップで圧縮・最適化された状態に変換されます。

処理① Babel(webpack)で、React.createElement()の形にトランスパイル
JSXをトランスパイルした実際のコードですが、
全体のコードは短縮します。(Child1、Child2は除くApp)
const App = () => {
return React.createElement("div", null,
React.createElement("h1", null, "App")
);
};
viteではesbuildが使われるので、違った形にトランスパイルされます。このような感じです。
const App = () => {
return _jsx("div", {
children: _jsx("h1", {
children: "App"
})
});
};
処理② 最終的には圧縮・最適化されたコードに変換
→ この変換後のコードはさらにminify(圧縮)され、ツリーシェイキング(コード内で使われていない部分を振り落とす)やデッドコード除去(プログラムの中で全く使われないコードを削除)といった最適化された状態に変換されます。
(以下の説明ではコードが複雑すぎるので圧縮される前のコードで話を進めます)
Step3 - クライアントとサーバー間通信
ここで一度全体の描画フローを確認したいと思います。
全体のコードは短縮します。(Child1、Child2は除く)
こんな感じのフローです。

- ビルド後のコードをサーバーに配置
- クライアント(ブラウザ)がHTMLを読み込み、そこからReactのJavaScriptを取得
- ReactDOM.createRoot()が発火、Reactのルートノードを作成
- ReactDOM.createRoot().render() メソッドが発火、Reactコンポーネントの評価を開始
- React.createElement()が発火、仮想DOMが生成される
- 仮想DOMのデータからFiberノードが構築される
- FiberノードからリアルDOMが作成される
- 生成したリアルDOMをブラウザの実際のDOMツリーに差し込み描画する
クライアントに着地した時からのフローを見ていきましょう!
Step4 - createRoot() 初期化フェーズ
Reactの管理下にするDOM要素(=マウント先)を準備します。
①Reactアプリケーションが描画先(コンテナ(root))を指定する
zennのコード
// 描画先を指定
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
②React内部でFiberルート(root Fiber)を事前に生成して、renderを呼ぶ準備を整える。
createRoot()を呼び出した時点で、Reactは root Fiber を作成し、後続の処理に備えて基盤を構築します。
rootFiberはここのコードで確認できます↓
setTimeout(() => {
// Fiberルート(root Fiber)
const rootFiber = root._internalRoot.current;
console.log(rootFiber);
}, 500);
Step5 - render() レンダーフェーズ
ReactのUIを、指定したDOMコンテナ(さきほど作ったroot)に描画していきます。
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( // 描画
<React.StrictMode>
<App />
</React.StrictMode>
);
以下①、②はrender()内で行われる処理です。
①React.createElement()
仮想DOMを生成します。
React.createElement()が発火 → 仮想DOM生成の図↓

②仮想DOMのデータを元にFiberノードを構築
Fiberノードを構築します。

Fiberノードの構築における処理
-
リコンシリエーションが実行される(Fiberノード構築における処理全体そのものと考えて差し支えありません)
→ Fiberノードの構築、差分の比較、更新の記録などが行われる。 - 初回レンダリングは比較対象がないので比較の処理はスキップされます。
→ 初回レンダリング時でもリコンシリエーション自体は実行されているが、差分比較などの処理はスキップされる。 - 全ノードが新規作成扱いとなり、Fiberノードはそれぞれの要素(h1、divなど)として構築され、リアルDOMを構築するための準備を整えていきます。
Step6 コミットフェーズ
FiberノードをリアルDOMに変換、ブラウザに反映してきます。
Fiberノードに基づいて、ReactはリアルDOM(h1やdivなど)を生成します。
DOM操作はこの段階で実行されます(createElementなど)。
イメージはこちらです。
FiberノードからリアルDOMが構築される図↓

生成されたリアルDOMは、最終的にブラウザの画面に追加されて、ユーザーの目に見えるようになります。
差分更新について (簡単に...)
このような感じです。

-
ユーザーがUIをクリック → 状態が変更される
Reactは関連するコンポーネントのuseState()などのフックを使って状態(state)を更新します。 -
状態変更
特定のコンポーネントで状態が変わると、そのコンポーネントとその子孫コンポーネントだけが再レンダリングされる。 -
新しい仮想DOMの生成
影響を受けるコンポーネントの範囲(親→子)で、新しい仮想DOMが再生成される。
再生成の範囲は、全体ではなく変更のあったコンポーネント単位のみとなります。(Reactの再レンダリングの効率化)
この時の差分の比較は、初回レンダリング時に内部に保持していた旧Fiberノードを参照して行なっています。 -
Fiberノードツリーを再構築 & リコンシリエーション
新しく生成された仮想DOMを元に、新しいFiberノードを再構築していきます。
保持していた旧Fiberノードと比較して差分を検出(=リコンシリエーション)。
差分が見つかった部分に「更新フラグ」を付けます。
この時、再構築したFiberノードは次の更新に備えてReact内部に保持しておきます。 -
コミットフェーズ
Fiberノード(ツリー)からリアルDOMへの更新をします。
フラグが付いたノード(変更のあった部分)だけをリアルDOMに反映します。
DOM操作が行われて実際のDOMを更新します(実際は上書き)。 -
ブラウザに描画される
以上です。
気づいた点や修正点があれば随時更新していきます。
読んでいただきありがとうございました!
参考
・React公式 ・Babel ・Vite
Discussion