Fiber: архитектура согласования, приоритеты и фазы рендеринга
Fiber — внутренняя архитектура согласования React, представленная в React 16. Она разбивает рендеринг на мелкие прерываемые единицы, что сделало возможными конкурентный режим, хуки и Suspense. Понимание Fiber объясняет, почему компоненты должны быть чистыми, как работают приоритеты обновлений и почему порядок хуков критичен.
Содержание
- Что такое Fiber
- Какую проблему решает Fiber
- Структура Fiber-узла
- Дерево Fiber: три версии
- Две фазы рендеринга
- Приоритеты и конкурентный режим
- Как работает прерывание работы
- Fiber и хуки
- Практическое значение для разработчиков
- Vue-аналоги
- История и эволюция
- Часто задаваемые вопросы
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Что такое Fiber
Fiber — это внутренняя архитектура согласования (reconciliation) React, представленная в React 16 (2017). Название происходит от латинского fibra — «волокно». Каждый React-элемент (компонент, DOM-узел, текст) представлен внутренним объектом — Fiber-узлом, который хранит всю информацию, необходимую React для работы с этим элементом: его тип, пропсы, состояние, ссылки на родительский и дочерние узлы, побочные эффекты и приоритет обновления.
Проще говоря: Fiber — это «рабочая единица» React. Если React-элемент — это декларативное описание UI («что показать»), то Fiber-узел — это внутреннее представление, которое React использует, чтобы решить, как и когда обновить DOM.
JSX → React Element (описание) → Fiber Node (рабочая единица) → DOM Node (реальный элемент)
Fiber — это не публичный API. Вы никогда не создаёте Fiber-узлы вручную и не читаете их напрямую. Но понимание Fiber объясняет, почему React работает именно так, как работает — и почему конкурентные возможности (Suspense, transitions, useDeferredValue) вообще стали возможны.
Какую проблему решает Fiber
Stack Reconciler (до React 16)
До React 16 согласователь работал синхронно и рекурсивно. При обновлении состояния React обходил всё дерево компонентов сверху вниз, сравнивал виртуальный DOM и применял изменения к реальному DOM — всё в одном непрерывном вызове стека.
setState()
→ render Component
→ render Child1
→ render Grandchild1
→ render Grandchild2
→ render Child2
→ ...
→ diff
→ commit to DOM
Проблема: этот процесс был непрерывным и неделимым. Пока React обрабатывал обновление, основной поток браузера был заблокирован. Если дерево было большим (тысячи компонентов), анимации, прокрутка и ввод пользователя «замирали» — браузер не мог обработать события, пока React не закончит.
Это классическая проблема UI-фреймворков: долгая синхронная работа в главном потоке блокирует взаимодействие с пользователем.
Fiber Reconciler (React 16+)
Fiber решает эту проблему, разбивая работу на маленькие единицы (отдельные Fiber-узлы). Вместо одного гигантского рекурсивного вызова React обходит дерево итеративно, обрабатывая по одному узлу за раз. После обработки каждого узла React проверяет: есть ли более приоритетная работа? Если да — текущая работа приостанавливается, уступая место более важному обновлению (например, обработке клика пользователя).
setState()
→ Fiber: App (приоритет: normal)
→ Fiber: Child1 (приоритет: normal)
→ ⏸ ПРИОРИТЕТНОЕ ОБНОВЛЕНИЕ: клик пользователя
→ Fiber: InputField (приоритет: discrete/urgent)
→ ▶ Возобновление: Child1
→ Fiber: Grandchild1
→ Fiber: Grandchild2
→ Fiber: Child2
→ commit
Ключевые свойства Fiber:
| Свойство | Stack Reconciler | Fiber Reconciler |
|---|---|---|
| Непрерывность | Синхронный, неделимый | Прерываемый, возобновляемый |
| Обход дерева | Рекурсивный (стек) | Итеративный (связный список) |
| Приоритеты | Нет | Да (срочные vs. фоновые) |
| Блокировка потока | Блокирует | Уступает управление |
| Пауза/возобновление | Невозможны | Встроены в архитектуру |
Структура Fiber-узла
Каждый Fiber-узел — это JavaScript-объект, содержащий информацию об одном элементе компонента. Вот упрощённая структура:
{
// Идентификация
tag: FunctionComponent, // Тип: HostComponent, HostText, FunctionComponent, ClassComponent и т.д.
key: "user-123", // Уникальный ключ для согласования списков
type: UserProfile, // Функция компонента или строка для DOM-элементов ("div", "span")
// Дерево (связный список)
return: parentFiber, // Родительский узел (куда возвращаемся после обработки)
child: childFiber, // Первый дочерний узел
sibling: nextSiblingFiber, // Следующий брат (следующий дочерний узел того же родителя)
index: 0, // Позиция среди братьев
// Состояние и ссылки
memoizedState: hooksList, // Связанный список хуков (для функциональных компонентов)
memoizedProps: { name: "Alice" }, // Пропсы, использованные при последнем рендере
pendingProps: { name: "Bob" }, // Новые пропсы, требующие обработки
stateNode: domNode, // Ссылка на реальный DOM-узел (для host-компонентов)
// Эффекты
flags: Update | Placement, // Битовая маска побочных эффектов (Update, Placement, Deletion и т.д.)
subtreeFlags: ChildUpdate, // Суммарные флаги поддерева (оптимизация commit-фазы)
deletions: [], // Массив Fiber-узлов, которые нужно удалить
// Альтернативное дерево
alternate: prevFiber, // Ссылка на узел из предыдущего дерева (для diffing)
// Приоритет
lanes: SyncLane, // Приоритет обновления (битовая маска «полос»)
childLanes: DefaultLane, // Суммарный приоритет поддерева
}
Связи между узлами
Fiber-узлы образуют дерево через связный список, а не через массив дочерних элементов. Каждый узел имеет три указателя:
App (root)
| (child)
Header
| (sibling)
Main
/ \
(child) (sibling → child)
Sidebar Content
child→ первый дочерний узелsibling→ следующий узел того же уровняreturn→ родительский узел
Такая структура позволяет обходить дерево итеративно, без рекурсии. React начинает с корня, идёт к child, затем к sibling, а когда детей больше нет — возвращается к return и идёт к его sibling. Это и есть «walk the tree» — обход дерева Fiber.
Дерево Fiber: три версии
В памяти React одновременно хранятся три дерева Fiber-узлов:
1. Current Tree (текущее дерево)
Дерево, которое сейчас отображается на экране. Это «истина» — то, что пользователь видит в данный момент.
2. Work-in-Progress Tree (рабочее дерево)
Дерево, которое React строит прямо сейчас, обрабатывая обновления. Это «черновик» — React обходит текущее дерево, создаёт или переиспользует Fiber-узлы в рабочем дереве, вычисляя, что должно измениться.
3. Committed Tree (зафиксированное дерево)
После завершения работы React «переключает» указатель: рабочее дерево становится текущим. Старое текущее дерево отбрасывается.
Текущее дерево (на экране) Рабочее дерево (в работе)
┌─────────────────────┐ ┌─────────────────────┐
│ App │ │ App (alternate) │
│ ├─ Header │ ←───→ │ ├─ Header (new) │
│ ├─ Sidebar │ │ ├─ Sidebar │
│ └─ Content │ │ └─ Content (new) │
└─────────────────────┘ └─────────────────────┘
▲ │
│ │
└──────── после commit ────────┘
рабочее становится текущим
Каждый Fiber-узел имеет поле alternate, указывающее на свой аналог в другом дереве. При обработке обновления React сравнивает current.alternate (предыдущее состояние узла) с current (новое состояние) — это и есть diffing.
Две фазы рендеринга
Работа React делится на две чётко разделённые фазы:
Фаза 1: Render Phase (фаза рендеринга) — «вычисление»
Может прерываться. В этой фазе React:
- Обходит дерево Fiber-узлов (work-in-progress).
- Для каждого узла вызывает функцию компонента (или метод
render()классового компонента). - Сравнивает результат с предыдущим деревом (diffing через
alternate). - Помечает узлы флагами эффектов (
flags):Placement(вставить),Update(обновить),Deletion(удалить). - Вычисляет
subtreeFlags— суммарные флаги поддерева.
Важно: в этой фазе нет побочных эффектов. React не трогает DOM, не вызывает useEffect, не запускает ref-колбэки. Он только вычисляет, что нужно изменить. Именно поэтому эта фаза может быть прервана и перезапущена — она чистая (pure).
⚠️ Начиная с React 18, Render Phase может вызываться более одного раза для одного обновления. React может отбросить работу, начать заново с другим приоритетом или даже спекулятивно отрендерить несколько версий. Это означает, что функции компонентов должны быть чистыми — без мутаций, побочных эффектов и чтения «грязных» значений.
Фаза 2: Commit Phase (фаза фиксации) — «применение»
Не может прерываться. В этой фазе React:
- Берёт помеченные Fiber-узлы и применяет изменения к реальному DOM.
- Запускает
useLayoutEffect(синхронно, до отрисовки браузером). - Устанавливает ref-ссылки.
- Запускает
useEffect(асинхронно, после отрисовки браузером).
┌──────────────────────────────────────────────────────┐
│ Render Phase │
│ (прерываемая, чистая, без побочных эффектов) │
│ │
│ 1. Обход дерева Fiber │
│ 2. Вызов компонентов │
│ 3. Diffing (сравнение с alternate) │
│ 4. Разметка флагов эффектов │
│ │
│ Может быть прервана, перезапущена, отброшена │
└────────────────────────────┬─────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Commit Phase │
│ (непрерываемая, синхронная) │
│ │
│ 1. Before Mutation: getSnapshotBeforeUpdate │
│ 2. Mutation: применение изменений к DOM │
│ 3. Layout: useLayoutEffect, ref-колбэки │
│ 4. Passive: useEffect (асинхронно, после paint) │
│ │
│ Всегда завершается полностью │
└──────────────────────────────────────────────────────┘
Приоритеты и конкурентный режим
Fiber вводит понятие приоритетов (lanes — «полосы»). Каждое обновление получает приоритет, определяющий, как скоро оно должно быть обработано.
Уровни приоритета (упрощённо)
| Приоритет | Пример | Описание |
|---|---|---|
| SyncLane (синхронный) | flushSync, useLayoutEffect | Должен быть выполнен немедленно, синхронно |
| DiscreteLane (дискретный) | Клик, нажатие клавиши | Высокий приоритет — пользователь ждёт ответа |
| DefaultLane (обычный) | setState, useEffect | Стандартный приоритет |
| TransitionLane (переход) | startTransition, useTransition | Низкий приоритет — можно отложить |
| IdleLane (фоновый) | Предварительный рендер | Самый низкий — только когда ничего другого нет |
Как приоритеты работают на практике
Когда пользователь нажимает кнопку, React назначает этому обновлению DiscreteLane. Если в этот момент React уже обрабатывает обновление с TransitionLane (например, фильтрацию большого списка), он:
- Приостанавливает текущую работу (TransitionLane).
- Обрабатывает более приоритетное обновление (DiscreteLane — клик).
- Применяет результат клика к DOM (commit).
- Возобновляет отложенную работу (TransitionLane), начиная с того места, где остановился.
Timeline:
───────────────────────────────────────────────────────────
[Transition: фильтрация списка...]
↑ КЛИК! (DiscreteLane)
[Обработка клика → commit]
↓ Возобновление
[...фильтрация списка — продолжение]
───────────────────────────────────────────────────────────
Это и есть конкурентный рендеринг — возможность React работать над несколькими версиями UI одновременно, переключаясь между ними по приоритету.
Как работает прерывание работы
Ключевое отличие Fiber от Stack Reconciler — способность приостанавливать и возобновлять работу. Вот как это работает на уровне кода:
Итеративный обход вместо рекурсии
Внутри React есть функция workLoop, которая выглядит примерно так:
function workLoopSync() {
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
Разница:
- Синхронная версия обрабатывает все узлы без остановки.
- Конкурентная версия проверяет
shouldYield()после каждого узла. Если время вышло (браузеру нужно обработать события, отрисовать кадр) — цикл прерывается.
performUnitOfWork — обработка одного узла
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
// Фаза 1: Вычисление (begin)
let next = beginWork(current, unitOfWork, renderLanes);
// beginWork вызывает компонент, создаёт/обновляет дочерние Fiber-узлы
if (next === null) {
// Нет дочерних узлов — фаза 2: Завершение (complete)
completeUnitOfWork(unitOfWork);
return null; // Или следующий узел для обработки
}
return next; // Следующий узел для обработки (дочерний)
}
После обработки каждого узла React возвращается в workLoop, который проверяет shouldYield(). Если нужно уступить управление — работа приостанавливается. Переменная workInProgress сохраняет текущую позицию в дереве.
shouldYield — когда уступить управление
React использует MessageChannel и performance.now() для отслеживания времени. Если текущий кадр браузера (обычно ~5мс на 120fps или ~16мс на 60fps) уже использован — React уступает управление браузеру через scheduler.postMessage().
Fiber и хуки
Fiber-узел хранит состояние хуков в поле memoizedState. Для функциональных компонентов это связанный список хуков:
Fiber Node
└─ memoizedState → Hook1 → Hook2 → Hook3 → null
(queue) ↓ ↓ ↓
state effect ref
(useState) (useEffect) (useRef)
Каждый хук в списке — это объект:
{
memoizedState: 0, // Текущее значение (для useState — текущее state)
baseState: 0, // Базовое состояние
queue: { pending: ... }, // Очередь обновлений
next: nextHook, // Следующий хук в списке
}
Почему порядок хуков важен
Поскольку хуки хранятся как связанный список, React сопоставляет их по позиции. При каждом ререндере React проходит по списку memoizedState в том же порядке, в котором хуки были вызваны. Если порядок изменится (условный вызов хука) — React сопоставит хуки неправильно, что приведёт к багам.
// ✅ Правильно: хуки всегда вызываются в одном порядке
function Component() {
const [a, setA] = useState(0); // Hook #1
const [b, setB] = useState(0); // Hook #2
useEffect(() => {}); // Hook #3
}
// ❌ Неправильно: порядок может измениться
function Component({ condition }) {
if (condition) {
const [a, setA] = useState(0); // Может быть пропущен!
}
const [b, setB] = useState(0); // React думает, что это Hook #1
}
Хуки и прерывание
Когда React приостанавливает работу (из-за более приоритетного обновления), memoizedState сохраняет текущее состояние хуков. При возобновлении работы React продолжает с того же Fiber-узла с теми же данными хуков.
Практическое значение для разработчиков
Хотя Fiber — это внутренняя архитектура, его понимание объясняет множество поведенческих аспектов React:
1. Компоненты должны быть чистыми
Render Phase может вызываться несколько раз для одного обновления. Если в компоненте есть побочные эффекты (мутации, API-вызовы), они будут выполнены повторно:
// ❌ Плохо: побочный эффект в рендере
function Counter() {
const [count, setCount] = useState(0);
localStorage.setItem("count", count); // Будет вызван несколько раз!
return <h1>{count}</h1>;
}
// ✅ Хорошо: побочный эффект в useEffect
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
localStorage.setItem("count", count); // Вызывается после commit
}, [count]);
return <h1>{count}</h1>;
}
2. setState не обновляет DOM немедленно
Когда вы вызываете setState, React не обновляет DOM синхронно. Он планирует обновление, назначает приоритет и обрабатывает его в следующем цикле workLoop. DOM обновляется только после Commit Phase.
function Component() {
const [value, setValue] = useState(0);
const handleClick = () => {
setValue(42);
// DOM ещё НЕ обновлён здесь!
// React запланировал обновление с DefaultLane
// и обработает его в следующем цикле workLoop
};
}
3. useTransition снижает приоритет
useTransition помечает обновление как TransitionLane — низкий приоритет. React может прервать его для обработки более срочных обновлений (клики, ввод):
const [isPending, startTransition] = useTransition();
startTransition(() => {
setSearchQuery(input); // TransitionLane — можно прервать
});
// Если пользователь продолжит печатать, это обновление
// будет прервано и перезапущено с новыми данными
4. flushSync повышает приоритет
flushSync заставляет React обработать обновление синхронно (SyncLane), не дожидаясь следующего цикла:
import { flushSync } from "react-dom";
flushSync(() => {
setCount(c => c + 1);
});
// DOM обновлён синхронно — сразу после этой строки
5. useLayoutEffect запускается в Commit Phase
useLayoutEffect вызывается синхронно в Commit Phase, до отрисовки браузером. Это позволяет читать метрики макета и синхронно перерендеривать, предотвращая мерцание:
useLayoutEffect(() => {
const height = tooltipRef.current.getBoundingClientRect().height;
setPosition({ top: -height }); // Синхронный ререндер до paint
}, []);
6. Ошибки в Render Phase могут быть обработаны повторно
React 18+ может «повторить» рендер, если обнаружит ошибку. Это означает, что console.log в компоненте может быть вызван несколько раз:
function Component() {
console.log("render"); // Может быть вызван 2+ раза в StrictMode
return <div />;
}
В StrictMode (development) React намеренно рендерит каждый компонент дважды, чтобы помочь обнаружить побочные эффекты в Render Phase.
Vue-аналоги
| React Fiber | Vue | Комментарий |
|---|---|---|
| Fiber-узел | VNode (Virtual Node) | Оба — внутренние представления элементов |
| Render Phase | Рендер-функция компонента | Оба вычисляют новое виртуальное дерево |
| Commit Phase | Patch (применение к DOM) | Оба применяют diff к реальному DOM |
alternate | Не используется | Vue мутирует VNode на месте; React создаёт новое дерево |
| Приоритеты (lanes) | Scheduler (в Vue 3) | Vue 3 имеет встроенный планировщик с приоритетами |
| Прерывание работы | Suspense + async setup | Vue 3 поддерживает приостановку через async компоненты |
memoizedState (хуки) | Reactive Proxy | Vue отслеживает зависимости автоматически через Proxy |
workLoop | queueFlush (microtask) | Vue батчирует обновления через микрозадачи |
Ключевое различие: React использует связный список Fiber-узлов с итеративным обходом, что позволяет прерывать работу. Vue использует рекурсивный обход VNode-дерева с Proxy-реактивностью — обновления точечные (компонент перерендеривается только при изменении его зависимостей), но менее гибкие в плане приоритетов.
История и эволюция
| Год | Версия | Событие |
|---|---|---|
| 2013 | React 0.3 | Stack Reconciler — синхронный, рекурсивный |
| 2015 | React 0.14 | Начало разработки новой архитектуры |
| 2017 | React 16 | Fiber Reconciler — полная переработка согласователя |
| 2018 | React 16.3 | React.createRef, forwardRef, Strict Mode |
| 2019 | React 16.8 | Хуки — зависят от Fiber (порядок вызовов через связанный список) |
| 2022 | React 18 | Конкурентный рендеринг — useTransition, useDeferredValue, автоматический батчинг |
| 2024 | React 19 | Действия (Actions), хук use(), React Compiler |
| 2025 | React 19.2 | <Activity />, useEffectEvent, Performance Tracks |
Fiber был представлен в React 16 как «полная переработка под капотом» — без новых публичных API. Но именно Fiber сделал возможными все последующие инновации: хуки (16.8), Suspense (16.6, стабильный в 18), конкурентный рендеринг (18) и React Compiler (2025).
Часто задаваемые вопросы
Fiber — это то же самое, что виртуальный DOM?
Нет. Виртуальный DOM — это концепция представления реального DOM в памяти. Fiber — это архитектура согласования, которая использует виртуальный DOM. Fiber-узлы — это расширенная версия виртуальных узлов, содержащая дополнительную информацию для итеративного обхода, приоритетов и прерывания работы.
Могу ли я получить доступ к Fiber-узлам напрямую?
Технически да, но не следует. Fiber-узлы доступны через __reactFiber$ или __reactInternalInstance$ на DOM-элементах — это внутренние свойства, не являющиеся публичным API. Они могут измениться в любой версии. Для отладки используйте React DevTools.
Почему React не использует Web Workers для рендеринга?
Fiber позволяет React уступать управление основному потоку после каждого узла, давая браузеру возможность обработать события и отрисовать кадр. Web Workers не имеют доступа к DOM, и передача данных между потоками добавляет задержку. Fiber — это более эффективное решение для той же проблемы: разбиение длинной задачи на мелкие единицы.
Влияет ли Fiber на производительность моих приложений?
Сам по себе Fiber — это оптимизация, а не нагрузка. Он позволяет React быть более отзывчивым, особенно при больших деревьях. Для большинства приложений разница незаметна — но при тысячах компонентов конкурентный рендеринг значительно улучшает UX.
Что такое «reconciliation» (согласование)?
Согласование — это процесс сравнения двух деревьев Fiber (current и work-in-progress) для определения, какие изменения нужно применить к DOM. Алгоритм согласования использует эвристики:
- Элементы разных типов → полностью заменить.
- Элементы одного типа → обновить на месте, переиспользуя DOM-узлы.
- Ключи (
key) → определить, какие элементы в списке изменились, добавлены или удалены.
Что такое «lanes» (полосы)?
Lanes — это битовая маска приоритетов, используемая Fiber для определения порядка обработки обновлений. Каждое обновление назначается на определённую «полосу» (SyncLane, DefaultLane, TransitionLane и т.д.). React обрабатывает более приоритетные полосы первыми, прерывая менее приоритетные.
Как Fiber связан с React Compiler?
React Compiler (стабильный в 2025) автоматически мемоизирует компоненты и значения на этапе сборки. Он работает поверх Fiber — компилятор анализирует код и вставляет мемоизацию, а Fiber использует эту информацию для оптимизации Render Phase. Без Fiber автоматическая мемоизация была бы невозможна, потому что не было бы структуры данных для хранения мемоизированных значений между рендерами.
Ключевые тезисы для интервью
- Fiber — архитектура согласования React 16+, разбивающая рендеринг на прерываемые единицы (Fiber-узлы).
- Stack Reconciler был синхронным и рекурсивным; Fiber — итеративный и прерываемый через связный список.
- Каждый Fiber-узел хранит тип, пропсы, состояние, ссылки на child/sibling/return и флаги эффектов.
- React хранит три дерева: current (на экране), work-in-progress (в работе), committed (после apply).
- Render Phase — прерываемая и чистая, Commit Phase — непрерываемая и синхронная.
- Приоритеты (lanes) определяют порядок: Sync > Discrete > Default > Transition > Idle.
- Хуки хранятся как связанный список в
memoizedState— поэтому порядок вызова критичен. setStateне обновляет DOM немедленно — обновление планируется и обрабатывается в следующем workLoop.- Компоненты должны быть чистыми: Render Phase может вызываться несколько раз (StrictMode).
Заключение
Fiber — фундамент всей современной архитектуры React. Он сделал возможными хуки, конкурентный рендеринг, Suspense и React Compiler. Ключевые выводы: компоненты должны быть чистыми, setState — асинхронный, приоритеты управляют порядком обработки, а порядок хуков фиксирован. Понимание Fiber не требует доступа к внутреннему API, но объясняет поведение React на глубоком уровне.