Skip to content

第5章 Fiberアーキテクチャ - 並行処理による応答性の革新

はじめに

現代のWebアプリケーションにおける最大の課題の一つは、複雑で大規模なUIを維持しながら、シームレスなユーザー体験を提供することです。従来の同期的なレンダリングアプローチでは、大量のDOM操作が発生した際にメインスレッドが長時間ブロックされ、ユーザーインタラクション(クリック、スクロール、キーボード入力)への応答が著しく遅延する問題が不可避でした。

前章では、Virtual DOMの差分検出により効率的なDOM更新を実現しましたが、根本的な問題は依然として残っています。すなわち、レンダリング処理が「原子的(atomic)」に実行されるため、大規模なUIツリーの更新時には避けられないパフォーマンスボトルネックが発生します。

本章では、この課題を根本的に解決するReact 16で導入された革新的な「Fiber」アーキテクチャを実装します。Fiberは、レンダリング処理を細粒度の作業単位に分割し、優先度ベースのスケジューリングと協調的マルチタスキングを実現する、並行レンダリングシステムです。この技術により、60FPSを維持しながら複雑なUIの更新を処理し、真の意味でのスケーラブルなフロントエンドアプリケーションの構築が可能になります。

Fiberアーキテクチャの理論的基盤

並行計算理論からのアプローチ

Fiberは、並行計算理論における「協調的マルチタスキング」の概念をWebブラウザ環境に適用した画期的なアーキテクチャです。従来のスタックベースの再帰的レンダリングとは根本的に異なり、以下の重要な特性を持ちます:

  • 中断可能性(Interruptibility): 作業を任意の時点で中断し、高優先度タスクを先行実行
  • 再開可能性(Resumability): 中断された作業を後で正確に再開
  • 増分処理(Incremental Processing): 大規模な処理を小さな時間スライスに分割
  • 優先度制御(Priority-based Scheduling): タスクの重要度に応じた実行順序の動的調整

イベントループとフレーム予算の活用

ブラウザのイベントループは約16.67ms(60FPS)のフレーム予算を持ちます。Fiberアーキテクチャは、この予算内でレンダリング作業を実行し、残り時間をユーザーインタラクションやその他の重要なタスクに割り当てることで、一貫した応答性を保証します。

Fiberシステムの核心技術

1. 作業単位の原子化(Work Unit Atomization)

従来のモノリシックなレンダリングプロセスを、各Virtual DOMノードに対応する独立した作業単位に分解します。各作業単位は以下の特性を持ちます:

  • 有界実行時間: 最大実行時間が予測可能
  • 副作用の分離: 他の作業単位への影響を最小化
  • 状態の完全性: 中断時に一貫した状態を維持

2. 多層優先度システム(Multi-tier Priority System)

異なるタイプの更新に対して階層化された優先度を適用:

  • Immediate: 同期的なレイアウト更新(最高優先度)
  • UserBlocking: ユーザー入力への応答(高優先度)
  • Normal: 通常のデータ更新(中優先度)
  • Idle: バックグラウンド処理(低優先度)

3. 協調的プリエンプション(Cooperative Preemption)

明示的な yield ポイントを設けることで、OSレベルのプリエンプションに依存しない協調的なタスク切り替えを実現します。

4. 時間スライシング(Time Slicing)

requestIdleCallback APIを活用し、ブラウザのアイドル時間を効率的に利用する適応的スケジューリングを実装します。

Fiber構造の実装

1. 型定義

まず、Fiber構造に必要な型を定義します:

typescript
// fiber/types.ts

import { VNode } from '../types';

// 作業タイプの定義
export enum WorkTag {
  HostRoot = 0,      // ルート要素
  HostComponent = 1, // 通常のDOM要素
  FunctionComponent = 2, // 関数コンポーネント
  TextElement = 3    // テキスト要素
}

// 作業の優先度
export enum Priority {
  Idle = 0,      // アイドル時のみ実行
  Normal = 1,    // 通常の優先度
  UserBlocking = 2, // ユーザー操作をブロックしない
  Immediate = 3  // 即時実行
}

// 副作用のタイプ
export enum EffectTag {
  None = 0,
  Create = 1,    // 新規作成
  Update = 2,    // 更新
  Delete = 3     // 削除
}

// Fiber構造の定義
export interface Fiber {
  // 要素の識別情報
  tag: WorkTag;        // 作業タイプ
  type: string | Function | null; // 要素の型
  key: string | null;  // 要素のキー
  
  // DOM関連
  stateNode: Node | null; // 対応するDOM要素
  
  // Fiber間の関係
  return: Fiber | null;   // 親Fiber
  child: Fiber | null;    // 最初の子Fiber
  sibling: Fiber | null;  // 次の兄弟Fiber
  index: number;          // 兄弟間のインデックス
  
  // 作業情報
  pendingProps: any;      // 新しいprops
  memoizedProps: any;     // 前回のprops
  memoizedState: any;     // コンポーネントの状態
  
  // 副作用
  effectTag: EffectTag;   // 実行すべき副作用
  nextEffect: Fiber | null; // 次に副作用を適用するFiber
  
  // 代替ツリー
  alternate: Fiber | null; // 作業中の代替Fiber
}

// 作業単位の定義
export interface WorkUnit {
  fiber: Fiber;
  priority: Priority;
}

2. テストの作成

テスト駆動開発のアプローチに従い、まずFiberアーキテクチャのテストを作成します:

typescript
import { createElement } from '../src/createElement';
import { createFiberRoot, renderFiber } from '../src/fiber/core';
import { scheduleWork } from '../src/fiber/update';

describe('fiber', () => {
  beforeEach(() => {
    // テスト用のDOM環境をリセット
    document.body.innerHTML = '';
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('初期レンダリングが正しく行われること', () => {
    const container = document.createElement('div');
    document.body.appendChild(container);
    
    const vnode = createElement(
      'div',
      { className: 'container' },
      createElement('h1', null, 'Hello, Fiber!')
    );
    
    // Fiberルートを作成
    const fiberRoot = createFiberRoot(container);
    
    // レンダリングをスケジュール
    scheduleWork(fiberRoot, vnode);
    
    // タイマーを進めて非同期処理を完了
    jest.runAllTimers();
    
    expect(container.innerHTML).toBe('<div class="container"><h1>Hello, Fiber!</h1></div>');
  });

  it('更新が正しく行われること', () => {
    const container = document.createElement('div');
    document.body.appendChild(container);
    
    // 初期レンダリング
    const initialVNode = createElement(
      'div',
      { className: 'container' },
      createElement('h1', null, 'Initial Text')
    );
    
    const fiberRoot = createFiberRoot(container);
    scheduleWork(fiberRoot, initialVNode);
    jest.runAllTimers();
    
    // 更新
    const updatedVNode = createElement(
      'div',
      { className: 'container updated' },
      createElement('h1', null, 'Updated Text')
    );
    
    scheduleWork(fiberRoot, updatedVNode);
    jest.runAllTimers();
    
    expect(container.querySelector('div')?.className).toBe('container updated');
    expect(container.querySelector('h1')?.textContent).toBe('Updated Text');
  });

  it('大規模なツリーでもブラウザがフリーズしないこと', () => {
    const container = document.createElement('div');
    document.body.appendChild(container);
    
    // 大量の要素を含むVNodeを作成
    const createLargeTree = (depth: number, breadth: number): any => {
      if (depth <= 0) {
        return createElement('span', null, 'Leaf Node');
      }
      
      const children = [];
      for (let i = 0; i < breadth; i++) {
        children.push(createLargeTree(depth - 1, breadth));
      }
      
      return createElement('div', { className: `depth-${depth}` }, ...children);
    };
    
    const largeVNode = createLargeTree(5, 5); // 深さ5、幅5のツリー(数千ノード)
    
    // パフォーマンス計測のモック
    const performanceMock = {
      now: jest.fn()
    };
    
    // 最初の呼び出しでは0を返し、その後は5msずつ増加
    let time = 0;
    performanceMock.now.mockImplementation(() => {
      const currentTime = time;
      time += 5;
      return currentTime;
    });
    
    // グローバルのperformanceをモックに置き換え
    const originalPerformance = global.performance;
    global.performance = performanceMock as any;
    
    try {
      const fiberRoot = createFiberRoot(container);
      scheduleWork(fiberRoot, largeVNode);
      
      // タイマーを進めて非同期処理を完了
      jest.runAllTimers();
      
      // レンダリングが完了していることを確認
      expect(container.querySelectorAll('div').length).toBeGreaterThan(100);
      expect(container.querySelectorAll('span').length).toBeGreaterThan(100);
      
      // 作業が分割されていることを確認
      expect(performanceMock.now).toHaveBeenCalledTimes(expect.any(Number));
      expect(performanceMock.now.mock.calls.length).toBeGreaterThan(10);
    } finally {
      // 元のperformanceオブジェクトを復元
      global.performance = originalPerformance;
    }
  });
});

3. Fiberコアの実装

テストに基づいて、Fiberアーキテクチャの核となる部分を実装します:

typescript
// fiber/core.ts

import { Fiber, WorkTag, EffectTag } from './types';
import { VNode } from '../types';

// Fiberルートを作成する関数
export const createFiberRoot = (container: HTMLElement): Fiber => {
  const root: Fiber = {
    tag: WorkTag.HostRoot,
    type: null,
    key: null,
    stateNode: container,
    return: null,
    child: null,
    sibling: null,
    index: 0,
    pendingProps: null,
    memoizedProps: null,
    memoizedState: null,
    effectTag: EffectTag.None,
    nextEffect: null,
    alternate: null
  };
  
  return root;
};

// VNodeからFiberを作成する関数
export const createFiber = (
  vnode: VNode,
  returnFiber: Fiber,
  index: number
): Fiber => {
  // タグの決定
  let tag: WorkTag;
  if (typeof vnode.type === 'function') {
    tag = WorkTag.FunctionComponent;
  } else if (vnode.type === 'TEXT_ELEMENT') {
    tag = WorkTag.TextElement;
  } else {
    tag = WorkTag.HostComponent;
  }
  
  // Fiberの作成
  const fiber: Fiber = {
    tag,
    type: vnode.type,
    key: vnode.props.key || null,
    stateNode: null,
    return: returnFiber,
    child: null,
    sibling: null,
    index,
    pendingProps: vnode.props,
    memoizedProps: null,
    memoizedState: null,
    effectTag: EffectTag.Create,
    nextEffect: null,
    alternate: null
  };
  
  return fiber;
};

// 子VNodeからFiberツリーを構築する関数
export const reconcileChildren = (
  returnFiber: Fiber,
  children: VNode[]
): Fiber | null => {
  if (!children || children.length === 0) {
    return null;
  }
  
  let firstChild: Fiber | null = null;
  let previousSibling: Fiber | null = null;
  
  // 各子要素に対してFiberを作成
  children.forEach((child, index) => {
    const newFiber = createFiber(child, returnFiber, index);
    
    if (index === 0) {
      // 最初の子要素
      firstChild = newFiber;
    } else if (previousSibling) {
      // 前の兄弟要素と連結
      previousSibling.sibling = newFiber;
    }
    
    previousSibling = newFiber;
  });
  
  return firstChild;
};

// DOM要素を作成する関数
export const createDomElement = (fiber: Fiber): Node => {
  let domElement: Node;
  
  if (fiber.tag === WorkTag.TextElement) {
    // テキスト要素
    domElement = document.createTextNode(fiber.pendingProps.nodeValue);
  } else {
    // 通常のDOM要素
    domElement = document.createElement(fiber.type as string);
    
    // 属性を設定
    Object.keys(fiber.pendingProps || {}).forEach(key => {
      if (key === 'children') return;
      
      if (key.startsWith('on') && typeof fiber.pendingProps[key] === 'function') {
        // イベントハンドラ
        const eventType = key.toLowerCase().substring(2);
        (domElement as HTMLElement).addEventListener(eventType, fiber.pendingProps[key]);
      } else {
        // 通常の属性
        (domElement as HTMLElement).setAttribute(key, fiber.pendingProps[key]);
      }
    });
  }
  
  return domElement;
};

// Fiberツリーを実際のDOMに反映する関数
export const commitWork = (fiber: Fiber | null): void => {
  if (!fiber) return;
  
  // 親のDOM要素を探す
  let parentFiber = fiber.return;
  while (parentFiber && !parentFiber.stateNode) {
    parentFiber = parentFiber.return;
  }
  
  if (!parentFiber || !parentFiber.stateNode) return;
  
  const parentDom = parentFiber.stateNode;
  
  // 副作用に基づいてDOMを操作
  switch (fiber.effectTag) {
    case EffectTag.Create:
      if (!fiber.stateNode) {
        fiber.stateNode = createDomElement(fiber);
      }
      parentDom.appendChild(fiber.stateNode);
      break;
    
    case EffectTag.Update:
      if (fiber.stateNode) {
        // 属性の更新
        const oldProps = fiber.memoizedProps || {};
        const newProps = fiber.pendingProps || {};
        
        // 古い属性を削除
        Object.keys(oldProps).forEach(key => {
          if (key === 'children') return;
          
          if (key.startsWith('on') && typeof oldProps[key] === 'function') {
            // イベントリスナーの削除
            const eventType = key.toLowerCase().substring(2);
            (fiber.stateNode as HTMLElement).removeEventListener(eventType, oldProps[key]);
          } else if (!(key in newProps)) {
            // 新しいプロパティにない属性を削除
            (fiber.stateNode as HTMLElement).removeAttribute(key);
          }
        });
        
        // 新しい属性を設定
        Object.keys(newProps).forEach(key => {
          if (key === 'children') return;
          
          if (key.startsWith('on') && typeof newProps[key] === 'function') {
            // イベントリスナーの設定
            const eventType = key.toLowerCase().substring(2);
            (fiber.stateNode as HTMLElement).addEventListener(eventType, newProps[key]);
          } else if (oldProps[key] !== newProps[key]) {
            // 属性値が変わった場合のみ更新
            if (fiber.tag === WorkTag.TextElement) {
              (fiber.stateNode as Text).nodeValue = newProps.nodeValue;
            } else {
              (fiber.stateNode as HTMLElement).setAttribute(key, newProps[key]);
            }
          }
        });
      }
      break;
    
    case EffectTag.Delete:
      if (fiber.stateNode) {
        parentDom.removeChild(fiber.stateNode);
      }
      break;
  }
  
  // 子要素と兄弟要素に対しても再帰的に適用
  commitWork(fiber.child);
  commitWork(fiber.sibling);
};

// Fiberツリーをレンダリングする関数
export const renderFiber = (fiber: Fiber, vnode: VNode): void => {
  // ルートFiberの子要素を設定
  fiber.child = reconcileChildren(fiber, [vnode]);
  
  // DOMに反映
  commitWork(fiber.child);
};

4. Fiber更新スケジューラの実装

次に、Fiberの更新をスケジュールする部分を実装します:

typescript
// fiber/update.ts

import { Fiber, WorkUnit, Priority } from './types';
import { VNode } from '../types';
import { renderFiber } from './core';

// 作業キュー
const workQueue: WorkUnit[] = [];
let isWorking = false;
let nextUnitOfWork: Fiber | null = null;

// 作業をスケジュールする関数
export const scheduleWork = (fiber: Fiber, vnode: VNode, priority: Priority = Priority.Normal): void => {
  // 作業単位をキューに追加
  workQueue.push({
    fiber,
    priority
  });
  
  // 作業を開始
  requestIdleCallback(performWork);
};

// 作業を実行する関数
const performWork = (deadline: IdleDeadline): void => {
  // 作業中フラグを設定
  isWorking = true;
  
  try {
    // 次の作業単位がなければ、キューから取得
    if (!nextUnitOfWork && workQueue.length > 0) {
      // 優先度順にソート
      workQueue.sort((a, b) => b.priority - a.priority);
      
      // 最も優先度の高い作業を取得
      const nextWork = workQueue.shift();
      if (nextWork) {
        // レンダリングを開始
        renderFiber(nextWork.fiber, nextWork.fiber.pendingProps.children[0]);
      }
    }
    
    // 時間が残っている限り作業を続ける
    while (nextUnitOfWork && deadline.timeRemaining() > 1) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
    
    // 作業が残っている場合は次のフレームで続行
    if (nextUnitOfWork || workQueue.length > 0) {
      requestIdleCallback(performWork);
    }
  } finally {
    // 作業中フラグをリセット
    isWorking = false;
  }
};

// 作業単位を実行する関数
const performUnitOfWork = (fiber: Fiber): Fiber | null => {
  // 作業を実行(この部分は実際には複雑な処理)
  // ...
  
  // 次の作業単位を返す(深さ優先探索)
  if (fiber.child) {
    return fiber.child;
  }
  
  let nextFiber: Fiber | null = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.return;
  }
  
  return null;
};

// requestIdleCallbackのポリフィル
const requestIdleCallback = 
  window.requestIdleCallback ||
  function(callback: IdleRequestCallback) {
    const start = Date.now();
    return setTimeout(() => {
      callback({
        didTimeout: false,
        timeRemaining: () => Math.max(0, 50 - (Date.now() - start))
      });
    }, 1);
  };

アーキテクチャの詳細解析

データ構造の革新:Linked-List Based Tree

実装したFiber構造は、従来のツリー構造とは異なり、連結リストベースのツリー表現を採用しています:

  1. Fiber構造の特徴

    • return: 親ノードへの参照(スタックフレームの代替)
    • child: 最初の子ノードへの参照
    • sibling: 次の兄弟ノードへの参照
    • alternate: ダブルバッファリング用の代替ツリー

    この設計により、再帰呼び出しによるスタックオーバーフローを回避し、メモリ使用量を予測可能にします。

  2. 作業単位の定義と実行

    各Fiberノードは独立した作業単位として機能し、以下の段階で処理されます:

    • Begin Work: コンポーネントの処理と子ノードの生成
    • Complete Work: DOM要素の準備と副作用の収集
    • Commit Work: 実際のDOMへの反映
  3. 双方向スケジューリング

    • Render Phase: 中断可能な差分計算フェーズ
    • Commit Phase: 原子的なDOM更新フェーズ

    この分離により、長時間実行される差分計算を安全に中断でき、DOM更新の一貫性を保証します。

パフォーマンス特性の理論的分析

  • 時間計算量: O(n) - nは変更されたコンポーネント数
  • 空間計算量: O(h + w) - hはツリーの高さ、wは作業キューのサイズ
  • 並行性: 複数の更新を並行してスケジュール可能
  • 応答性: フレーム予算内での確定的な実行時間

Fiberツリーの構造

Fiberツリーの構造を図示します:

作業の流れ

Fiberアーキテクチャにおける作業の流れを図示します:

実際の使用例

Fiberアーキテクチャを使用して、大規模なUIを効率的に更新する例を示します:

typescript
import { createElement } from './createElement';
import { createFiberRoot } from './fiber/core';
import { scheduleWork } from './fiber/update';

// 大量の要素を含むリストコンポーネント
const LargeList = (props: { items: string[] }) => {
  const listItems = props.items.map((item, index) => 
    createElement('li', { key: index }, item)
  );
  
  return createElement('ul', { className: 'large-list' }, ...listItems);
};

// アプリケーションコンポーネント
const App = (props: { items: string[] }) => {
  return createElement(
    'div',
    { className: 'container' },
    createElement('h1', null, 'Large List Example'),
    createElement(LargeList, { items: props.items })
  );
};

// 初期データ
let items = Array(1000).fill(0).map((_, i) => `Item ${i + 1}`);

// コンテナ要素
const container = document.getElementById('root');

if (container) {
  // Fiberルートを作成
  const fiberRoot = createFiberRoot(container);
  
  // 初期レンダリング
  const vnode = createElement(App, { items });
  scheduleWork(fiberRoot, vnode);
  
  // 定期的に更新(例: 1秒ごと)
  setInterval(() => {
    // データを更新
    items = [...items.slice(1), `Item ${Date.now()}`];
    
    // 再レンダリング
    const newVNode = createElement(App, { items });
    scheduleWork(fiberRoot, newVNode);
  }, 1000);
}

本章のまとめと次世代への展開

革新的成果の総括

本章では、Fiberアーキテクチャの実装により、フロントエンド開発における根本的なパラダイムシフトを実現しました:

技術的達成

  • スケーラビリティの確保: 数千のコンポーネントを含む大規模UIでも60FPSを維持
  • 応答性の保証: ユーザーインタラクションへの一貫した低レイテンシ応答
  • 予測可能性: フレーム予算に基づく確定的なパフォーマンス特性
  • 拡張性: 優先度システムの柔軟な拡張とカスタマイズ

理論的貢献

  • 協調的マルチタスキングのWebブラウザ環境への適用
  • 連結リストベースツリーによるメモリ効率の最適化
  • 時間スライシングによる応答性とスループットのバランス最適化

アーキテクチャの限界と今後の課題

しかしながら、現在の実装にはさらなる進化の余地があります:

  1. 状態管理の複雑化: コンポーネント間の状態共有とライフサイクル管理
  2. 副作用の制御: 非同期処理やブラウザAPIとの連携
  3. メモ化戦略: 不要な再計算を避ける効率的なキャッシング
  4. デバッグ性: 非同期処理における問題の特定と診断

次章への技術的発展

これらの課題を解決し、Fiberアーキテクチャの真の力を発揮するため、次章では「Hooks」システムを実装します。Hooksは関数コンポーネントに状態管理と副作用処理の能力を付与し、以下の先進的な機能を提供します:

  • 宣言的状態管理: useStateによる直感的な状態操作
  • 副作用の制御: useEffectによるライフサイクルとの統合
  • パフォーマンス最適化: useMemouseCallbackによる計算の最適化
  • カスタムロジックの再利用: ユーザー定義Hooksによる横断的関心事の抽象化

この組み合わせにより、宣言的でありながら高性能、かつ保守性の高い現代的なWebアプリケーション開発の基盤が完成します。

Released under the MIT License.