Skip to content

6.2 Reconciliation と Fiber ​

6.1 で見たように、React は Virtual DOM の前後差分を計算して実 DOM に反映します。この章ではその差分計算を担うエンジン Reconciler と、その内部データ構造 Fiber を、概念レベルで覗いていきます。

この章は実装詳細に踏み込まず、なぜこの形になっているか の理解を目的にしています。実装は React のバージョンにより変化するので、引用するソース位置はあくまで参考情報として扱ってください。

Reconciler の役割 ​

Reconciler(リコンサイラー) は、React のコアで差分計算を担っているモジュールです。ソースツリーでは packages/react-reconciler/(v19.2.5 タグ時点)以下に置かれています。

React が 1 回の再レンダーで行うことを、単純化すると次のようになります。

  1. 新しい React Element ツリー(Virtual DOM)を計算する
  2. Reconciler が「既存の内部ツリー」と「新しい Element ツリー」を突き合わせる
  3. 差分(どの DOM を追加・更新・削除するか)を決める
  4. 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 オブジェクトです。主要なフィールドだけを抜き出すと、こんな形です。

text
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 ツリー ​

current(画面に出ている)AppHeaderListItem AItem Bwork-in-progress(計算中)App'Header'List'Item A'Item B'alternateCommit 後: work-in-progress が次の current になる(double buffering)

なぜ配列ではなくポインタなのかと言うと、中断・再開ができるように するためです。配列を深く再帰で辿ってしまうと、途中で止めたときに呼び出しスタックの状態を保存・復元する必要が出てきます。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 ノードずつ処理していきます。擬似コードで書くと、こんな雰囲気です。

js
// 擬似コード(実装そのままではありません)
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 ずつ進むこと、途中で止められること、そして再開できることが体感できるはずです。

Phase待機中
中断回数: 0
-App
-Header
-List
-Item #1
-Item #2
-Footer
work loop のログ
「Render 開始」を押すと、1 Fiber ずつ処理が流れます。途中で「高優先の割り込み」を押すと、work loop が中断します。

途中で割り込みを入れると、計算中だった 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 の連結リスト を見ていきます。

さらに深く学ぶには ​

実装レベルの挙動に興味がある方へ:

参照ソース ​