До версии 16 ядро React использовало рекурсивный механизм сравнения виртуальных деревьев — Stack Reconciler. При любом обновлении состояния движок синхронно обходил всё дерево компонентов сверху вниз через обычный стек вызовов JavaScript (Call Stack).
Если дерево содержало тысячи узлов, операция блокировала основной поток браузера на 50–200 мс. В этот период браузер не мог реагировать на пользовательский ввод, обновлять CSS-анимации или производить отрисовку кадров, что вызывало заметный джиттер и выпадение кадров (Frame Drops).
Архитектура React Fiber полностью переработала эту парадигму. Вместо неявного стека вызовов движка Fiber реализует виртуальный стек в куче (heap-allocated virtual stack frame), где каждый элемент дерева представлен структурой данных FiberNode. Это позволило:
В отличие от классического DOM-дерева с массивами потомков (children: [...]), внутренний граф Fiber представляет собой структуру связного списка: каждый узел знает только своего первого ребенка (child), следующего соседа (sibling) и родительский узел (return).
[ Current Tree (Отображается в DOM) ] [ WorkInProgress Tree (Собирается в фоне) ]
FiberRoot FiberRoot
│ │
(current pointer) (workInProgress)
▼ ▼
[FiberNode: App] <====== alternate ======> [FiberNode: App]
/ ▲ / ▲
child/ \return child/ \return
▼ \ ▼ \
[FiberNode: Header] \ [FiberNode: Header] \
│ \ │ \
sibling\ \ sibling\ \
▼ \ ▼ \
[FiberNode: Main] ─────┘ [FiberNode: Main] ─────┘Такая структура дает фундаментальное инженерное преимущество: граф обходится в плоском цикле while без рекурсии. В любой момент времени рантайм может сохранить указатель на текущую ноду, освободить поток браузеру для отрисовки кадра, а затем возобновить вычисления ровно с прерванного места.
Ядро React держит в оперативной памяти два параллельных дерева:
workInProgress дерево за одну атомарную операцию FiberRoot.current = workInProgress со сложностью O(1).Двойная буферизация гарантирует, что пользователь никогда не увидит полусобранный интерфейс. Если рендеринг тяжелого поддерева прерывается ошибкой или вытесняется более приоритетным вводом, недособранное дерево workInProgress просто отбрасывается без ущерба для отображаемого экрана.
Пользователь инициирует действие. Планировщик генерирует битовую маску события (например, SyncLane или TransitionLane) и помечает путь от целевого узла к корню FiberRoot через childLanes.
Создается или переиспользуется узел workInProgress Root через свойство alternate, копируя входные параметры текущего смонтированного дерева.
Обход графа сверху вниз. Вызов функций компонентов, вычисление нового состояния, проверка необходимости оптимизации (bailout) и запуск диффинга детей.
Создание и обновление инстансов DOM-нод в памяти, навешивание пропсов, сборка очередей эффектов и всплытие битовой маски subtreeFlags.
Синхронное применение мутаций к DOM: вызов деструкторов useLayoutEffect, манипуляция DOM-узлами, атомарное переключение ссылки FiberRoot.current и запуск пассивных useEffect.
Почему ядро React Fiber отказалось от классического дерева с массивами потомков (children: [...]) в пользу связных списков (child, sibling, return)?
Рендеринг в React строго разделен на две концептуальные фазы: Render Phase (асинхронная, прерываемая, свободная от побочных эффектов) и Commit Phase (синхронная, непрерываемая, применяющая изменения к DOM).
Генерация битовой маски события (например, SyncLane для ввода или TransitionLane для фильтра) и пометка пути от целевого узла к корню FiberRoot через childLanes.
Выполнение дискретных задач performUnitOfWork. Спуск вниз через beginWork и подъем через completeWork с контролем дедлайна shouldYield().
Сравнение текущего FiberNode и нового JSX элемента. Определение флагов побочных эффектов (Placement, Update, ChildDeletion).
Синхронное применение дерева мутаций: вызов деструкторов useLayoutEffect, мутация DOM-нод, переключение указателя current, запуск useEffect.
Каждый узел FiberNode создается фабричной функцией ядра. Ниже представлена строгая типизация ключевых полей структуры в React 19:
export type WorkTag =
| 0 // FunctionComponent
| 1 // ClassComponent
| 2 // IndeterminateComponent
| 3 // HostRoot (Root node)
| 5 // HostComponent (<div>, <span>)
| 6 // HostText
| 11 // ForwardRef
| 13 // SuspenseComponent
| 24; // CacheComponent
export type Flags = number; // Битовая маска мутаций (Placement, Update, Deletion)
export type Lane = number; // 31-битная маска приоритета
export type Lanes = number;
export interface FiberNode {
// Идентификация и дескриптор
tag: WorkTag;
key: null | string;
elementType: unknown;
type: unknown;
stateNode: unknown; // Ссылка на реальный DOM-узел или инстанс компонента
// Указатели связного списка графа
return: FiberNode | null; // Родительский узел (куда поднимаемся при завершении)
child: FiberNode | null; // Первый дочерний узел
sibling: FiberNode | null; // Следующий соседний узел
index: number;
// Входные и сохраненные данные
pendingProps: Record<string, unknown>; // Пропсы для текущей обработки
memoizedProps: Record<string, unknown>; // Пропсы, примененные в предыдущем рендере
memoizedState: unknown; // Связный список хуков (Hook linked list)
updateQueue: unknown; // Очередь обновлений состояния
// Приоритеты и эффекты
flags: Flags; // Локальные сайд-эффекты (мутации) данного узла
subtreeFlags: Flags; // Агрегированные эффекты дочернего поддерева
deletions: Array<FiberNode> | null; // Дочерние узлы, намеченные к удалению
lanes: Lanes; // Приоритеты ожидающих обновлений данного узла
childLanes: Lanes; // Приоритеты ожидающих обновлений в поддереве
// Механизм двухбуферности
alternate: FiberNode | null; // Указатель на зеркальную ноду (current <-> workInProgress)
}Выполнение фазы рендеринга координируется функцией workLoopConcurrent. Метод shouldYield() опирается на планировщик Scheduler, использующий нативный MessageChannel:
import { shouldYield } from './schedulerHostConfig';
let workInProgress: FiberNode | null = null;
let renderLanes: Lanes = 0;
export function workLoopConcurrent(): void {
// Цикл исполняется до тех пор, пока есть задачи и браузер не требует вернуть управление
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: FiberNode): void {
const current = unitOfWork.alternate;
// 1. Фаза спуска: вызов функционального компонента, диффинг детей
let next: FiberNode | null = beginWork(current, unitOfWork, renderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 2. Если дочерних узлов нет, переходим к фазе подъема
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
function completeUnitOfWork(unitOfWork: FiberNode): void {
let completedWork: FiberNode | null = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber: FiberNode | null = completedWork.return;
// Сборка DOM-свойств, агрегация subtreeFlags снизу вверх
completeWork(current, completedWork, renderLanes);
const siblingFiber: FiberNode | null = completedWork.sibling;
if (siblingFiber !== null) {
// Если есть сосед, переключаем фокус на него для спуска
workInProgress = siblingFiber;
return;
}
// Иначе поднимаемся к родителю
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}В отличие от ранней концепции expirationTime, где приоритет был одномерным числом (что порождало блокировку независимых задач), Lanes представляют собой бинарные дорожки. Всего в 32-битном числе выделяется 31 полоса:
export const TotalLanes = 31;
export const NoLanes: Lanes = 0b0000000000000000000000000000000;
export const NoLane: Lane = 0b0000000000000000000000000000000;
// Синхронные блокирующие полосы (дискретный ввод: клик, клавиатура)
export const SyncLane: Lane = 0b0000000000000000000000000000001;
// Непрерывный ввод (скролл, перемещение курсора)
export const InputContinuousLane: Lane = 0b0000000000000000000000000000010;
// Стандартные обновления по умолчанию (setState, ответы сети)
export const DefaultLane: Lane = 0b0000000000000000000000000010000;
// Переходы Transition (startTransition, useDeferredValue)
export const TransitionLanes: Lanes = 0b0000000011111111111111100000000;
export const TransitionLane1: Lane = 0b0000000000000000000000100000000;
// Фоновые простаивающие полосы
export const IdleLanes: Lanes = 0b0110000000000000000000000000000;
/**
* Получение наивысшего приоритета (крайний правый установленный бит)
* Битовая магия: x & -x возвращает наименьший значащий бит в дополнительном коде
*/
export function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes;
}
/**
* Проверка пересечения полос
*/
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane): boolean {
return (set & subset) === subset;
}
/**
* Объединение полос
*/
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
/**
* Удаление обработанных полос
*/
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
return set & ~subset;
}Выражение lanes & -lanes использует представление отрицательных чисел в дополнительном коде (Two's Complement). Число -lanes математически эквивалентно ~lanes + 1. Побитовая операция AND обнуляет все биты, кроме младшего единичного бита (Least Significant Bit), за один процессорный такт, позволяя планировщику мгновенно находить самый срочный Lane.
React решает фундаментальную задачу сравнения двух списков детей за линейное время O(N) вместо классического O(N^3) с помощью двух эвристических проходов:
key). При первом же несовпадении проход прерывается.Map<Key | Index, FiberNode>. Новые элементы ищутся в этой карте за O(1). Если узел найден, он переиспользуется с флагом Placement, а удаленные узлы помечаются флагом ChildDeletion.interface ChildElement {
key: string | null;
type: unknown;
props: Record<string, unknown>;
}
export function reconcileChildrenArray(
returnFiber: FiberNode,
currentFirstChild: FiberNode | null,
newChildren: ChildElement[],
lanes: Lanes
): FiberNode | null {
let resultingFirstChild: FiberNode | null = null;
let previousNewFiber: FiberNode | null = null;
let oldFiber = currentFirstChild;
let nextOldFiber: FiberNode | null = null;
let lastPlacedIndex = 0;
let newIdx = 0;
// ПРОХОД 1: Идем по порядку, пока совпадают ключи
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newChild = newChildren[newIdx];
// Если ключ изменился, первый проход завершается досрочно
if (oldFiber.key !== newChild.key) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
const matchedFiber = updateSlot(returnFiber, oldFiber, newChild, lanes);
if (matchedFiber === null) {
break;
}
lastPlacedIndex = placeChild(matchedFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = matchedFiber;
} else {
previousNewFiber.sibling = matchedFiber;
}
previousNewFiber = matchedFiber;
oldFiber = nextOldFiber;
}
// Если новые элементы закончились, удаляем оставшиеся старые
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// ПРОХОД 2: Если старые ноды остались, помещаем их в Map для O(1) поиска
if (oldFiber !== null) {
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newChild = newChildren[newIdx];
const existingFiber = existingChildren.get(
newChild.key !== null ? newChild.key : newIdx
) || null;
const newFiber = updateFromMap(existingFiber, returnFiber, newChild, lanes);
if (newFiber !== null) {
if (existingFiber !== null) {
existingChildren.delete(newChild.key !== null ? newChild.key : newIdx);
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// Все оставшиеся в Map ноды помечаются флагом ChildDeletion
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
function placeChild(newFiber: FiberNode, lastPlacedIndex: number, newIndex: number): number {
newFiber.index = newIndex;
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// Элемент сдвинулся вправо относительно своего прошлого окружения
newFiber.flags |= 0b0000000000000000000000000000010; // Placement
return lastPlacedIndex;
}
return oldIndex;
}
// Новый узел, вставляемый впервые
newFiber.flags |= 0b0000000000000000000000000000010;
return lastPlacedIndex;
}
function updateSlot(
returnFiber: FiberNode,
oldFiber: FiberNode,
newChild: ChildElement,
lanes: Lanes
): FiberNode | null {
if (oldFiber.key === newChild.key && oldFiber.elementType === newChild.type) {
return useFiber(oldFiber, newChild.props);
}
return null;
}
function updateFromMap(
existingFiber: FiberNode | null,
returnFiber: FiberNode,
newChild: ChildElement,
lanes: Lanes
): FiberNode | null {
if (existingFiber !== null && existingFiber.elementType === newChild.type) {
return useFiber(existingFiber, newChild.props);
}
return createFiberFromElement(newChild, lanes);
}
function mapRemainingChildren(
returnFiber: FiberNode,
currentFirstChild: FiberNode
): Map<string | number, FiberNode> {
const existingChildren = new Map<string | number, FiberNode>();
let existing: FiberNode | null = currentFirstChild;
while (existing !== null) {
const key = existing.key !== null ? existing.key : existing.index;
existingChildren.set(key, existing);
existing = existing.sibling;
}
return existingChildren;
}
function useFiber(fiber: FiberNode, pendingProps: Record<string, unknown>): FiberNode {
const clone = fiber.alternate || { ...fiber };
clone.pendingProps = pendingProps;
return clone;
}
function createFiberFromElement(child: ChildElement, lanes: Lanes): FiberNode {
return {
tag: 0,
key: child.key,
elementType: child.type,
type: child.type,
stateNode: null,
return: null,
child: null,
sibling: null,
index: 0,
pendingProps: child.props,
memoizedProps: {},
memoizedState: null,
updateQueue: null,
flags: 0b0000000000000000000000000000010,
subtreeFlags: 0,
deletions: null,
lanes,
childLanes: 0,
alternate: null
};
}
function deleteChild(returnFiber: FiberNode, childToDelete: FiberNode): void {
returnFiber.flags |= 0b0000000000000000000000000010000; // ChildDeletion
if (returnFiber.deletions === null) {
returnFiber.deletions = [childToDelete];
} else {
returnFiber.deletions.push(childToDelete);
}
}
function deleteRemainingChildren(returnFiber: FiberNode, currentFirstChild: FiberNode | null): void {
let child: FiberNode | null = currentFirstChild;
while (child !== null) {
deleteChild(returnFiber, child);
child = child.sibling;
}
}Что происходит внутри алгоритма reconcileChildrenArray во втором проходе, если у элемента массива сменилась позиция с индекса 5 на индекс 0, но сохранился стабильный key?
Для интроспекции внутренних структур Fiber в runtime-среде отладки создадим легковесный инспектор, позволяющий извлечь приоритеты полос и статус ноды непосредственно из инстанса DOM:
interface FiberDebugSnapshot {
tag: number;
key: string | null;
typeName: string;
lanes: number;
lanesBinary: string;
hasPendingWork: boolean;
childCount: number;
}
const REACT_FIBER_PREFIX = '__reactFiber$';
export function getFiberFromDom(domElement: HTMLElement): FiberNode | null {
const fiberKey = Object.keys(domElement).find(key => key.startsWith(REACT_FIBER_PREFIX));
if (!fiberKey) {
return null;
}
return ((domElement as unknown) as Record<string, FiberNode>)[fiberKey] || null;
}
export function inspectFiberSubtree(fiber: FiberNode): FiberDebugSnapshot {
let childCount = 0;
let currentChild = fiber.child;
while (currentChild !== null) {
childCount++;
currentChild = currentChild.sibling;
}
const typeName =
typeof fiber.type === 'function'
? fiber.type.name || 'AnonymousFunction'
: typeof fiber.type === 'string'
? fiber.type
: 'Fragment/Special';
return {
tag: fiber.tag,
key: fiber.key,
typeName,
lanes: fiber.lanes,
lanesBinary: fiber.lanes.toString(2).padStart(31, '0'),
hasPendingWork: fiber.lanes !== 0 || fiber.childLanes !== 0,
childCount
};
}При сортировке, фильтрации или вставке элементов в начало массива использование индекса key={index} заставляет алгоритм Reconciliation сопоставлять совершенно разные сущности в первом проходе. Fiber ошибочно переиспользует старый FiberNode с чужим memoizedState, вызывая баги рассинхронизации локального стейта инпутов и лишние DOM-мутации.
// ❌ ПЛОХО: смещение локального стейта инпутов и сброс фокуса
{items.map((user, index) => (
<UserRow key={index} data={user} />
))}
// ✅ ПРАВИЛЬНО: стабильный уникальный идентификатор сущности из базы
{items.map((user) => (
<UserRow key={user.id} data={user} />
))}Если компонент объявляется внутри функции рендера родителя, на каждом цикле рендеринга создается новая ссылка на функцию elementType. В алгоритме диффинга проверка oldFiber.elementType === newChild.type возвращает false. В результате React полностью уничтожает старый подграф Fiber (Unmount со всеми очистками useEffect) и монтирует новый с нуля, теряя всё состояние.
// ❌ ПЛОХО: пересоздание типа на каждом рендере, полный сброс DOM-дерева
export function OrderManager() {
const StatusBadge = ({ active }: { active: boolean }) => (
<span className={active ? 'green' : 'red'} />
);
return <StatusBadge active={true} />;
}
// ✅ ПРАВИЛЬНО: вынесение компонента в стабильную область модуля
const StatusBadge = ({ active }: { active: boolean }) => (
<span className={active ? 'green' : 'red'} />
);
export function OrderManager() {
return <StatusBadge active={true} />;
}Метод flushSync прерывает кооперативную работу планировщика и немедленно форсирует выполнение workLoopSync для всех накопившихся обновлений, очищая SyncLane. Злоупотребление flushSync внутри обработчиков событий или циклов блокирует UI-поток и провоцирует выпадение кадров (Frame Drops).
import { flushSync } from 'react-dom';
// ❌ ПЛОХО: форсирование синхронного полного рендера на каждый шаг
function handleBatchUpdates(items: string[]) {
items.forEach(item => {
flushSync(() => {
setList(prev => [...prev, item]);
});
});
}
// ✅ ПРАВИЛЬНО: пакетное автобатчирование React 19 без вмешательства в поток
function handleBatchUpdates(items: string[]) {
setList(prev => [...prev, ...items]);
}Поскольку фаза Render прерываема и может перезапускаться несколько раз до завершения кадра, любые прямые манипуляции со stateNode (DOM) или внешними объектами внутри тела компонента приведут к неконсистентному состоянию интерфейса и утечкам памяти. Любые DOM-мутации допустимы только в фазе Commit (useEffect, useLayoutEffect).