6.2 Reconciliation と Fiber
6.1 で見たように、React は Virtual DOM の前後差分を計算して実 DOM に反映します。この章ではその差分計算を担うエンジン Reconciler と、その内部データ構造 Fiber を、概念レベルで覗いていきます。
この章は実装詳細に踏み込まず、なぜこの形になっているか の理解を目的にしています。実装は React のバージョンにより変化するので、引用するソース位置はあくまで参考情報として扱ってください。
Reconciler の役割
Reconciler(リコンサイラー) は、React のコアで差分計算を担っているモジュールです。ソースツリーでは packages/react-reconciler/(v19.2.5 タグ時点)以下に置かれています。
React が 1 回の再レンダーで行うことを、単純化すると次のようになります。
- 新しい React Element ツリー(Virtual DOM)を計算する
- Reconciler が「既存の内部ツリー」と「新しい Element ツリー」を突き合わせる
- 差分(どの DOM を追加・更新・削除するか)を決める
- Renderer(
react-domなど)がその差分を実 DOM に反映する
ここで出てくる「既存の内部ツリー」が、これから説明する Fiber ツリー です。
Reconciler 自体はプラットフォームから切り離されていて、ブラウザの DOM でも React Native でも、その他のレンダリングターゲット(React ART など)でも共通に使われます。具体的な DOM 操作は「host config」と呼ばれるインターフェース越しに、Renderer 側に任せる設計になっています。
Fiber とは何か
Fiber は、Reconciler が扱う「仕事 (work) の単位」を表すデータ構造 です。Virtual DOM の 1 ノードに対して、内部的には 1 つの Fiber ノードが対応します。
Andrew Clark による設計ドキュメント(補助資料)では、こう説明されています。
A fiber represents a unit of work. (中略) Fiber is reimplementation of the stack, specialized for React components. You can think of a single fiber as a virtual stack frame. — React Fiber Architecture(補助資料: React コア開発者 Andrew Clark による設計メモ)
NOTE: unverified — 要確認: Fiber アーキテクチャが main ブランチに取り込まれた時期は React 16 系(2017 年)とされますが、正確なマイルストーン日付はソースから再確認が必要です。
Fiber ノードの構造
Fiber ノードは、ツリーを構築するためのポインタ群と、レンダー処理に関する情報を持った JavaScript オブジェクトです。主要なフィールドだけを抜き出すと、こんな形です。
FiberNode {
type, // 'div' や関数コンポーネントなど、元の React Element の type
key, // key prop(リスト要素の同一性判定用)
return, // 親 Fiber へのポインタ
child, // 最初の子 Fiber へのポインタ
sibling, // 次の兄弟 Fiber へのポインタ
alternate, // もう一方の Fiber ツリー(current 版 / work-in-progress 版)へのポインタ
pendingProps, // これから反映する props
memoizedProps, // 前回反映した props
memoizedState, // フック等の内部状態(Hook リストの先頭もここ)
}注目したいのは次の 2 点です。
- 親子関係を表現するのに、通常の配列ではなく
return/child/siblingのポインタ を使っている alternateを持っていて、「画面に出ているツリー (current)」と「計算中のツリー (work-in-progress)」の 2 本を同時に保持できる
図 6.2.1 Fiber ノードのポインタ関係
図 6.2.2 current ツリーと work-in-progress ツリー
なぜ配列ではなくポインタなのかと言うと、中断・再開ができるように するためです。配列を深く再帰で辿ってしまうと、途中で止めたときに呼び出しスタックの状態を保存・復元する必要が出てきます。Fiber はノード自身に親兄弟へのポインタを持たせることで、ある Fiber を処理し終えた瞬間に次のノードへ進めばよいだけの形になっており、処理を中断して別の仕事に譲ることができるのです。
なぜ Fiber が必要になったのか
Fiber が解こうとしたのは、単純な再帰走査による Reconciler が抱える制約 です。コンポーネントツリーを深い再帰で辿る実装だと、1 回のレンダーを途中で止められません。大きなツリーの再計算に 50ms かかると、その間ブラウザのメインスレッドはブロックされ、ユーザーの入力やアニメーションがカクつきます。
Fiber が解決したい課題は次のようなものです。
- レンダー作業を中断し、後で再開できる
- 更新ごとに優先度を付ける(ユーザー入力 > データフェッチ結果の反映 など)
- 作業を破棄したり、再利用したりできる
Andrew Clark の設計メモでは、こんな感じで動機が整理されています。
Its main goals are:
- Ability to split interruptible work in chunks.
- Ability to prioritize, rebase and reuse work in progress.
- Ability to yield back and forth between parents and children to support layout in React.
- Ability to return multiple elements from
render.- Better support for error boundaries. — React Fiber Architecture(補助資料)
この「中断可能な作業単位」という発想が、のちに useTransition や useDeferredValue(4.1 参照)といった優先度付き更新の API を支える土台になっています。
work loop と 2 フェーズ
Reconciler は、Fiber ツリーを work loop と呼ばれるループで 1 ノードずつ処理していきます。擬似コードで書くと、こんな雰囲気です。
// 擬似コード(実装そのままではありません)
let nextUnitOfWork = rootFiber;
function workLoop(deadline) {
while (nextUnitOfWork && deadline.timeRemaining() > 0) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork) {
// まだ作業が残っている → 次のアイドル時間で続きをやる
requestIdleCallbackLike(workLoop);
} else {
// すべての Fiber を処理し終えた → コミットへ
commitRoot();
}
}肝心なのは、performUnitOfWork が 1 つの Fiber を処理し終えたらいったん return する 点です。ブラウザに残り時間がなければループを抜け、ブラウザのアイドル時に再度呼ばれます。ここが「中断可能なレンダー」の正体ですね。
このループ全体は、2 つのフェーズに分かれています。
- Render Phase: Fiber ツリーを走査して、work-in-progress ツリーを組み立てる。中断可能。副作用は起こさない
- Commit Phase: 完成した work-in-progress ツリーをもとに実 DOM を更新する。中断不可(途中で止めると UI が中途半端な状態になるため)
触って確かめる: work loop の中断と再開
下のデモで、「Render 開始」→ 途中で「高優先の割り込み」→「再開」と押してみてください。work loop が 1 Fiber ずつ進むこと、途中で止められること、そして再開できることが体感できるはずです。
途中で割り込みを入れると、計算中だった Fiber は一旦 pending に戻ります。これは「作業を破棄して、高優先の仕事を先にやる」挙動をシミュレートしたものです。現実の React では、破棄した work-in-progress を元に、新しい入力を反映した形で再度レンダーを始めるイメージになります。
図 6.2.3 Render Phase と Commit Phase の往復
Commit Phase が終わると、alternate ポインタが付け替わり、work-in-progress ツリーが次回の current ツリーになります。この「2 本のツリーを交互に使う」方式を double buffering と呼びます。
優先度と Scheduler
Fiber には各仕事の 優先度 を扱う仕組みがあり、React の Scheduler モジュールと連携して「即座に処理すべき更新」と「遅らせてよい更新」を振り分けています。
- ユーザー入力のような 同期的で高優先 の更新は、即座に処理される
useTransitionで包んだ更新やuseDeferredValueの結果は 低優先 として扱われ、高優先の仕事があれば後回しにされる
低優先の Render Phase が走っている最中にユーザー入力が来ると、Reconciler は計算途中の work-in-progress を 破棄 して、新しい入力を反映した高優先レンダーを先に処理できます。Render Phase を純粋に保つことが大事なのは、こうした 破棄と再試行 を安全に行うためでもあります。
NOTE: unverified — 要確認: Scheduler の優先度レベル名(ImmediatePriority, UserBlockingPriority など)は実装詳細のため変遷があります。最新の名称は facebook/react packages/scheduler(v19.2.5 タグ時点)のソースを参照してください。
まとめ
- Reconciler は、前回の内部ツリーと新しい React Element ツリーの差分を計算するエンジン
- Fiber は Reconciler が扱う「仕事の単位」。Virtual DOM の 1 ノードに対応するオブジェクト
- Fiber ノードは
return/child/sibling/alternateといったポインタで接続される - Fiber 導入の動機は レンダーを中断可能にすること
- work loop は 1 Fiber ずつ処理し、ブラウザの残り時間がなくなれば中断する
- 全体は Render Phase(中断可) と Commit Phase(中断不可) の 2 段階
useTransition/useDeferredValueの「優先度を下げる」挙動は、この Fiber + Scheduler の仕組みで実現されている
次章では、この Fiber ノードの中に格納されている Hook の連結リスト を見ていきます。
さらに深く学ぶには
実装レベルの挙動に興味がある方へ:
- React Fiber Architecture — Andrew Clark — React コア開発者本人による設計メモ。補助資料として定評があります
- packages/react-reconciler — facebook/react (v19.2.5) — Reconciler の実装本体
- packages/scheduler — facebook/react (v19.2.5) — 優先度付きスケジューラ
参照ソース
- Render and Commit — react.dev
- Preserving and Resetting State — react.dev
- packages/react-reconciler — facebook/react (v19.2.5 タグ)
- 補助資料: Andrew Clark, React Fiber Architecture — React コア開発者による設計メモ
- 補助資料: Dan Abramov, React as a UI Runtime