ReactのレンダリングパイプラインにおけるFiberツリーの役割
Reactアプリケーションのパフォーマンス最適化には、Fiberアーキテクチャの理解が不可欠です。特にレンダリングフェーズにおけるツリー構築プロセスは、状態変更時のUI更新効率を決定づけます。以下のコード例を考察してみましょう:
import React, { useState } from 'react';
const ValueCounter = () => {
const [value, setValue] = useState(0);
return (
<section>
<button onClick={() => setValue(prev => prev + 1)}>
カウント増加
</button>
<h3 key={value}>上{value}</h3>
<h2 key={value}>中{value}</h2>
<h1 key={value}>下{value}</h1>
</section>
);
};
export default ValueCounter;
このコンポーネントでボタンをクリックした際のDOM変更挙動は直感的でしょうか?key属性の同一性がレンダリング結果に与える影響を、Fiberツリーの内部処理から解説します。
Reactの三層アーキテクチャ
Reactフレームワークは状態入力からUI出力への変換を実現します。この処理は以下の三層で構成されます:
- スケジューラー:高優先度タスクの配信を管理
- レンダリングフェーズ(協調エンジン):変更検出とFiberノードへのフラグ設定
- コミットフェーズ(レンダリングエンジン):DOM操作の実行
状態変更が発生すると、スケジューラーがタスクを登録し、レンダリングフェーズでFiberツリーを再構築。変更差分を反映した後、コミットフェーズで実DOMに適用されます。
ReactElement・Fiber・DOMの関係性
JSX構文はコンパイル時にReact.createElement()に変換され、ReactElementオブジェクトを生成します。このオブジェクトはFiberノードに変換され、最終的にDOMノードと対応付けられます。
// Fiberノードの主要プロパティ
interface FiberNode {
nodeType: string; // ノード種別(div, component等)
domElement: HTMLElement | null; // 実DOM参照
parent: FiberNode | null; // 親ノード
children: FiberNode[]; // 子ノード配列
pendingProps: object; // 次回レンダリング用プロップス
effectFlags: number; // 変更フラグ(追加/削除/更新)
}
Fiberツリーは双方向連結リスト構造を持ち、親子関係と兄弟関係をポインタで管理します。各ノードは実DOMへの参照と、次回レンダリングに必要な状態を保持します。
初期化処理の内部フロー
アプリ起動時のReactDOM.createRoot()実行で以下のオブジェクトが生成されます:
- RootContainer:DOMルート要素のラッパー
- FiberRoot:グローバル状態管理オブジェクト
- HostRoot:Fiberツリーのルートノード
初期化完了時点で、ReactElementは独立した状態で存在し、Fiberツリーとの関連付けはまだ行われていません。レンダリングフェーズ開始時に初めて要素ツリーがFiberノードに変換されます。
双バッファリングによるFiber構築
レンダリングプロセスでは2つのFiberツリーが同時に存在します:
- Currentツリー:現在表示中のツリー(
fiberRoot.current参照) - WorkInProgressツリー:構築中のツリー(
HostRoot.alternate参照)
ツリー構築は深さ優先探索で実行され、各ノードで以下の2フェーズを経ます:
1. 探索フェーズ(beginWork)
- ReactElementからFiberノードを生成
- ノード間のポインタ関係を構築(親・子・兄弟)
- 変更フラグを設定(
effectFlags)
2. 帰還フェーズ(completeWork)
- 実DOMノードの生成と属性設定
- 副作用キューの構築(追加/削除/更新操作)
- 親ノードへの副作用キューの統合
要素更新時の差分検出アルゴリズム
状態変更時の再レンダリングでは、既存ツリーとの差分検出が実行されます。主要な最適化ルール:
- 同階層ノードのみを比較対象とする
- 要素タイプが異なる場合はツリー再構築
- key属性で移動要素を特定
// 多要素差分検出のロジック
function reconcileChildren(
prevNodes: FiberNode[],
nextElements: ReactElement[]
): FiberNode[] {
const result: FiberNode[] = [];
const existingMap = new Map();
// 1回目:共通シーケンスの探索
for (let i = 0; i < Math.min(prevNodes.length, nextElements.length); i++) {
if (prevNodes[i].key === nextElements[i].key) {
result.push(updateNode(prevNodes[i], nextElements[i]));
} else {
break;
}
}
// 2回目:残存要素の処理
nextElements.slice(result.length).forEach(element => {
const existing = existingMap.get(element.key);
if (existing) {
result.push(moveNode(existing, element));
} else {
result.push(createNode(element));
}
});
return result;
}
key属性が同一の要素は再利用されますが、例題のコードではすべての要素が同一keyを持つため、差分検出時に予期せぬ動作が発生します。要素タイプが異なる