React Fiberツリーの初期化と更新メカニズムの詳細分析

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出力への変換を実現します。この処理は以下の三層で構成されます:

  1. スケジューラー:高優先度タスクの配信を管理
  2. レンダリングフェーズ(協調エンジン):変更検出とFiberノードへのフラグ設定
  3. コミットフェーズ(レンダリングエンジン):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ノードの生成と属性設定
  • 副作用キューの構築(追加/削除/更新操作)
  • 親ノードへの副作用キューの統合

要素更新時の差分検出アルゴリズム

状態変更時の再レンダリングでは、既存ツリーとの差分検出が実行されます。主要な最適化ルール:

  1. 同階層ノードのみを比較対象とする
  2. 要素タイプが異なる場合はツリー再構築
  3. 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を持つため、差分検出時に予期せぬ動作が発生します。要素タイプが異なる

が同一keyで定義されている場合、Reactはノードの再利用を試みますが、タイプ不一致によりツリー再構築が発生します。

タグ: React Fiber Virtual DOM Reconciliation Key属性 レンダリング最適化

7月31日 00:49 投稿