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行目)として LayoutStatic、PassiveStatic、ViewTransitionStatic なども存在します。レンダリングのたびにリセットされる動的フラグと異なり、静的フラグは Fiber のライフタイムを通じて保持されます。これらは「その Fiber が特定の種類のエフェクトをいつかでも使う」ことを示し、アンマウント時の最適化を可能にします。PassiveStatic が一度もセットされていなければ、passive エフェクトのクリーンアップを求めてサブツリーをトラバースする必要がありません。
FiberRoot とルートの生成
Fiber ノードがツリー内のコンポーネントを表す一方、FiberRoot はツリー全体のスケジューリングとライフサイクルの状態を保持するコンテナレベルのデータ構造です。
FiberRoot は FiberRootNode によって生成され、以下の情報を持ちます:
current:コミット済みのルート Fiber(HostRoot fiber)containerInfo:DOM コンテナ要素pendingLanes、suspendedLanes、pingedLanes、expiredLanes:スケジューリング用のレーン追跡callbackNode、callbackPriority:スケジューラとの統合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 を呼び出し、その結果をスクリーンにコミットします。