Read OSS

Fiber データ構造 — React の内部表現

上級

前提知識

  • 第1回:アーキテクチャ概要(パッケージの配置と fork システムの仕組みを把握していること)
  • JavaScript のビット演算(&、|、~、>>>、clz32)
  • ツリーデータ構造とリンクリストのトラバーサル

Fiber データ構造 — React の内部表現

あなたが書くすべての React コンポーネント、レンダリングするすべての <div>、ラップするすべての Suspense バウンダリ — それらは実行時にすべて Fiber ノード へと変換されます。Fiber は素のJavaScript オブジェクトです(V8 のシェイプ最適化のためにコンストラクタ関数で生成されます)。そこには React がひとつの作業単位を処理するために必要なすべての情報が詰まっています。型、props、state、ツリー内での位置、そしてホストにコミットすべきエフェクトの情報です。

Fiber の型を理解することは、このシリーズ以降の内容すべての前提知識になります。ワークループは Fiber をトラバースし、フックは Fiber 上に state を保持し、コミットフェーズは Fiber からフラグを読み取ります。Fiber が React のランタイム世界における原子だとすれば、この記事はその周期表です。

Fiber の型定義

Fiber 型の全定義は ReactInternalTypes.js にあります。フィールドを論理的なグループに分けて見ていきましょう。

識別フィールド — この Fiber は何者か?

フィールド 用途
tag WorkTag 数値型の識別子(FunctionComponent、HostComponent など)
key ReactKey reconciliation のためにユーザーが指定するキー
elementType any React element の生の type(reconciliation 中の同一性を保持)
type any 解決済みの関数・クラス・文字列。ホットリロード時に elementType と異なる場合がある
stateNode any HostComponent では DOM ノード、ClassComponent ではクラスインスタンス、HostRoot では FiberRoot

ツリーリンク — この Fiber はどこに位置するか?

フィールド 用途
return Fiber | null 親 Fiber(スタックフレームの戻り先アドレスにちなんで "return" と命名)
child Fiber | null 最初の子 Fiber
sibling Fiber | null 次の兄弟 Fiber
index number 兄弟の中での位置

入出力 — どんなデータがこの Fiber を流れるか?

フィールド 用途
pendingProps any 現在のレンダリング用の props
memoizedProps any 最後に完了したレンダリング時の props
memoizedState any 最後に完了したレンダリング時の state(フックの場合はフックのリンクリストの先頭)
updateQueue mixed 保留中の state 更新キュー

スケジューリングとエフェクト — React はいつ、どのようにこの Fiber を処理するか?

フィールド 用途
lanes Lanes 保留中の作業優先度ビットマスク
childLanes Lanes サブツリー内の保留中の作業
flags Flags この Fiber に対するサイドエフェクト
subtreeFlags Flags サブツリー全体で集約されたサイドエフェクト
deletions Array<Fiber> | null 削除予定の子要素
alternate Fiber | null もう一方のツリーにある対応 Fiber(ダブルバッファリング)

ツリー構造 — child、sibling、return

React は子要素を配列で保持しません。代わりに単方向リンクリストを使います。Fiber の child最初の子を指し、各子の sibling次の子を指します。そしてすべての Fiber の return は親へと戻ります。

次の JSX を例に考えてみましょう:

<App>
  <Header />
  <Main />
  <Footer />
</App>

結果として生成される Fiber ツリーは次のようになります:

graph TD
    App["App<br/>tag: FunctionComponent"]
    Header["Header<br/>tag: FunctionComponent"]
    Main["Main<br/>tag: FunctionComponent"]
    Footer["Footer<br/>tag: FunctionComponent"]

    App -->|child| Header
    Header -->|sibling| Main
    Main -->|sibling| Footer
    Header -->|return| App
    Main -->|return| App
    Footer -->|return| App

このリンクリスト設計により、ワークループ(第3回で解説)は再帰なし・配列アロケーションなしでツリーをトラバースできます。アルゴリズムの流れはシンプルです。child へ移動して処理し、sibling リンクをたどり切ったら return で親に戻り、親の sibling へと続けます。

FiberNode コンストラクタ では、これらのリンクは null で初期化されます:

// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;

補足: このフィールドが parent でなく return と呼ばれているのは、コールスタックのメタファーに由来します。Fiber を処理することは関数を呼び出すことに似ており、処理が終わったら呼び出し元に「return(戻る)」します。この命名を内面化すると、ワークループの動作が直感的に理解できるようになります。

ダブルバッファリング — current と workInProgress

React はツリー内のすべての Fiber について、常に2つのバージョンを保持します。current ツリーは現在スクリーンにコミット済みのもの、workInProgress ツリーはレンダリング中に構築されているものです。

各 Fiber は alternate ポインタで、もう一方のツリーにある対応 Fiber を指しています。createWorkInProgress 関数は、既存の alternate を再利用するか、新しい Fiber を生成します:

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    // ... set up alternate links
  } else {
    // Reuse the existing alternate, reset its fields
    workInProgress.pendingProps = pendingProps;
    // ...
  }
}
sequenceDiagram
    participant C as Current Tree
    participant W as WorkInProgress Tree
    participant S as Screen

    Note over C,S: Initial state: current is committed
    C->>W: createWorkInProgress (clone fibers)
    Note over W: Render phase: modify WIP tree
    W->>C: commitRoot: swap current pointer
    Note over C,S: WIP becomes the new current
    C->>S: DOM mutations applied

このダブルバッファリング技法こそが、コンカレントレンダリングを可能にしている基盤です。workInProgress ツリーが変更される間も、current ツリーはイベントシステムや DevTools などから安定して参照可能な状態を保ちます。コミット時に React は root.current を更新するだけで、どちらのツリーが「current」かを入れ替えます。

WorkTag — 30 種類の Fiber 型

すべての Fiber は tag フィールドを持ちます。これは ReactWorkTags.js で定義された数値定数で、その Fiber がどの種類のコンポーネントを表すかを識別します。beginWork 関数(第3回で解説)はこのタグに対する大きな switch 文を使って、適切な処理ロジックへディスパッチします。

タグをカテゴリごとにまとめると次のとおりです:

タグ カテゴリ
FunctionComponent 0 基本コンポーネント
ClassComponent 1 基本コンポーネント
HostRoot 3 ホスト要素
HostPortal 4 ホスト要素
HostComponent 5 ホスト要素(例:<div>
HostText 6 ホスト要素
HostHoistable 26 ホスト要素(例:<link><script>
HostSingleton 27 ホスト要素(例:<html><head><body>
Fragment 7 構造
Mode 8 構造
ContextProvider 10 コンテキスト
ContextConsumer 9 コンテキスト
SuspenseComponent 13 バウンダリ
SuspenseListComponent 19 バウンダリ
ActivityComponent 31 バウンダリ(旧 Offscreen)
MemoComponent 14 最適化
SimpleMemoComponent 15 最適化(デフォルト比較の関数コンポーネント)
LazyComponent 16 最適化
ForwardRef 11 ラッパー
Profiler 12 開発ツール
ViewTransitionComponent 30 トランジション
Throw 29 エラーハンドリング

2 はスキップされています(かつての IndeterminateComponent で React 19 で削除済み)。値 20 も存在しません。

エフェクトフラグとフェーズマスク

各 Fiber の flags フィールドは ReactFiberFlags.js で定義された 31 ビットのビットマスクです。このフラグがコミットフェーズに対して、どのサイドエフェクトを実行すべきかを伝えます。

flowchart TD
    subgraph DF["Dynamic Flags"]
        P["Placement (0b10)<br/>Insert into DOM"]
        U["Update (0b100)<br/>Apply prop changes"]
        CD["ChildDeletion (0b10000)<br/>Remove children"]
        R["Ref (0b1000000000)<br/>Attach/detach refs"]
        PA["Passive (0b100000000000)<br/>useEffect"]
        V["Visibility (0b10000000000000)<br/>Show/hide"]
    end

    subgraph PhasM["Phase Masks"]
        BM["BeforeMutationMask<br/>Snapshot"]
        MM["MutationMask<br/>Placement | Update | ChildDeletion<br/>| Ref | Hydrating | Visibility"]
        LM["LayoutMask<br/>Update | Callback | Ref | Visibility"]
        PM["PassiveMask<br/>Passive | Visibility | ChildDeletion"]
    end

    DF --> PhasM

フェーズマスクは重要な最適化の鍵です。コミットフェーズでは、React はツリーを複数回走査します(before-mutation、mutation、layout、passive)。各サブフェーズはそれぞれが関心を持つフラグを定義したマスクを持っています。subtreeFlags フィールドはサブツリー全体のフラグを集約しているため、関連するエフェクトが存在しないサブツリーをまるごとスキップできます。

たとえば MutationMask は次のように定義されています:

export const MutationMask =
  Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility | FormReset;

ある Fiber の subtreeFlags & MutationMask がゼロであれば、mutation サブフェーズはそのサブツリー全体をスキップします。

また、静的フラグ(67〜88行目)として LayoutStaticPassiveStaticViewTransitionStatic なども存在します。レンダリングのたびにリセットされる動的フラグと異なり、静的フラグは Fiber のライフタイムを通じて保持されます。これらは「その Fiber が特定の種類のエフェクトをいつかでも使う」ことを示し、アンマウント時の最適化を可能にします。PassiveStatic が一度もセットされていなければ、passive エフェクトのクリーンアップを求めてサブツリーをトラバースする必要がありません。

FiberRoot とルートの生成

Fiber ノードがツリー内のコンポーネントを表す一方、FiberRoot はツリー全体のスケジューリングとライフサイクルの状態を保持するコンテナレベルのデータ構造です。

FiberRoot は FiberRootNode によって生成され、以下の情報を持ちます:

  • current:コミット済みのルート Fiber(HostRoot fiber)
  • containerInfo:DOM コンテナ要素
  • pendingLanessuspendedLanespingedLanesexpiredLanes:スケジューリング用のレーン追跡
  • callbackNodecallbackPriority:スケジューラとの統合
  • next:保留中の作業を持つルートのリンクリストを形成するためのリンク
  • finishedWork:コミット待ちの完成済み workInProgress ツリー

FiberRoot と HostRoot fiber の関係は双方向です:

graph LR
    FR["FiberRoot<br/>(container-level state)"]
    HR["HostRoot Fiber<br/>(tag: 3)"]
    FR -->|"current"| HR
    HR -->|"stateNode"| FR
    HR -->|"child"| App["App Fiber"]

createRoot(container) を呼び出すと、DOM レンダラーは createFiberRoot を通じて FiberRoot と初期の HostRoot fiber を生成します。HostRoot fiber は特別な存在で、ツリーの最上位に常に位置し、レンダリングの「エントリーポイント」となる Fiber です。

モードビット

各 Fiber は mode フィールドも持っています。これは ReactTypeOfMode.js で定義された小さなビットマスクです:

export const NoMode =            0b0000000;
export const ConcurrentMode =    0b0000001;
export const ProfileMode =       0b0000010;
export const StrictLegacyMode =  0b0001000;
export const StrictEffectsMode = 0b0010000;

モードビットは親から子へと継承され、生成後は変更されません。ConcurrentMode はタイムスライシングを有効にし、ProfileMode は Profiler のタイミング計測を有効にし、StrictEffectsMode は開発環境でのエフェクト二重実行を有効にします。

補足: React の内部をデバッグする際に最も役立つフィールドは、tag(何の Fiber か)、memoizedState(現在の state またはフックリスト)、flags(保留中のエフェクト)、lanes(スケジュール済みの優先度)です。

次回予告

Fiber データ構造について — そのツリー形状、ダブルバッファリングのライフサイクル、型タグ、フラグシステム — を理解できたところで、いよいよそれが動く様子を追っていきましょう。次回はワークループを解剖します。ワークループは React の鼓動するコアであり、Fiber ツリーを走査しながら下りで beginWork を、上りで completeWork を呼び出し、その結果をスクリーンにコミットします。