Механика рендеринга: Virtual DOM, Reconciliation и Fiber
Механика рендеринга: Virtual DOM, Reconciliation и Fiber
Парадокс: изменение одного класса у <div> через нативный JavaScript занимает микросекунды. Зачем же тогда React вводит огромную прослойку абстракций, которая добавляет время на выполнение JS-кода? Ответ кроется в том, что мы разбирали в первом курсе: сам по себе JavaScript невероятно быстр, а вот вызовы браузерных процессов Layout и Paint — непозволительно дороги. React был создан не для того, чтобы быстрее выполнять код, а чтобы минимизировать общение с реальным DOM, группируя изменения.
В этой статье мы разберем, как именно React решает эту задачу, почему старый алгоритм рендеринга начал приводить к зависанию интерфейсов и как архитектура Fiber превратила React в операционную систему для компонентов.
Virtual DOM: Иллюзия скорости
Браузерный DOM (Document Object Model) — это тяжеловесная структура. Каждый узел содержит десятки свойств и методов, а изменение узла может запустить каскадный перерасчет геометрии страницы (Layout).
Virtual DOM (VDOM) — это легковесная копия реального DOM, хранящаяся в оперативной памяти (в Memory Heap движка V8). По сути, это обычный JavaScript-объект. Когда вы пишете JSX, он компилируется в вызовы функции React.createElement(), которая возвращает объекты такого вида:
const element = {
type: 'div',
props: {
className: 'container',
children: [
{ type: 'h1', props: { children: 'Hello' } }
]
}
};
Создание и изменение тысяч таких объектов в памяти занимает доли миллисекунды и никак не затрагивает процессы рендеринга браузера. Когда состояние компонента меняется, React не бежит сразу обновлять страницу. Он создает новое дерево Virtual DOM и сравнивает его со старым.
Этот процесс поиска отличий называется Reconciliation (согласование).
Reconciliation: Искусство находить отличия
В информатике классический алгоритм сравнения двух деревьев имеет сложность , где — количество элементов. Для приложения из 1000 элементов это означало бы миллиард операций при каждом рендере. React использует эвристический алгоритм, который снижает сложность до , опираясь на два допущения.
Правило 1: Элементы разных типов порождают разные деревья
Если в процессе обновления тег <a> сменился на <img>, или компонент <Header /> сменился на <Footer />, React даже не будет пытаться искать сходства внутри их дочерних элементов.
Он полностью уничтожит старый узел (вызвав размонтирование) и создаст новый с нуля. Это радикально отсекает лишние ветви для сравнения.
Правило 2: Идентификация через ключи (Keys)
Когда React сравнивает списки дочерних элементов, он итерируется по ним одновременно. Если в начало списка добавляется новый элемент, React по умолчанию увидит несовпадение на первой же позиции и перестроит весь список целиком, что крайне неэффективно.
Здесь на помощь приходит атрибут key. Он сообщает React: «Этот элемент — тот же самый, что был в прошлом рендере, просто он изменил позицию».
Результатом фазы Reconciliation становится список патчей (изменений) — какие узлы нужно добавить, удалить или обновить в реальном DOM.
Проблема Stack Reconciler: Пробка в Main Thread
До 16-й версии React использовал архитектуру, которую сейчас называют Stack Reconciler. Процесс обхода дерева и сравнения Virtual DOM был рекурсивным.
Из курса по асинхронности мы помним, как работает Call Stack (стек вызовов). Рекурсивный обход дерева означает, что функции кладутся в Call Stack одна за другой. Пока рекурсия не дойдет до конца дерева и стек не опустеет, движок JavaScript не отдаст управление браузеру.
Если дерево большое, фаза Reconciliation могла занимать 50, 100 или 200 миллисекунд. Это классический Long Task. Поскольку Main Thread заблокирован, браузер пропускает кадры (теряет заветные 16.6 мс на кадр), анимации дергаются, а клики пользователя не обрабатываются. React не мог сказать: «Я сравнил половину дерева, давай отрисуем кадр, а остальное я доделаю потом». Стек вызовов V8 нельзя поставить на паузу.
React Fiber: Собственный Call Stack
Чтобы решить проблему блокировки потока, команде React пришлось переписать ядро с нуля. Если нативный Call Stack нельзя прервать, значит, React должен реализовать свой собственный виртуальный стек, которым он сможет управлять.
Этой архитектурой стал React Fiber.
Fiber (волокно) — это структура данных, представляющая единицу работы. По сути, это обычный JS-объект, содержащий информацию о компоненте, его состоянии и том, что нужно сделать.
Главное отличие Fiber от старой архитектуры — отказ от рекурсии в пользу связного списка (Linked List). Каждый узел Fiber имеет ссылки на:
child(первого ребенка)sibling(соседнего элемента)return(родителя)
Такая структура позволяет обходить дерево в цикле while, а не через рекурсию. А цикл можно прервать в любой момент!
Теперь React работает так:
- Берет один Fiber-узел, выполняет для него работу (вызывает функцию компонента, сравнивает VDOM).
- Проверяет, не истекло ли время, выделенное браузером (обычно около 5 мс).
- Если время вышло, React сохраняет указатель на текущий узел и возвращает управление Main Thread. Браузер отрисовывает кадр, обрабатывает события.
- В следующем цикле Event Loop React возобновляет работу ровно с того места, где остановился.
Этот механизм дробления работы называется Time-slicing (нарезка времени).
Две фазы рендеринга
Внедрение Fiber позволило разделить процесс обновления экрана на две строгие фазы, понимание которых критически важно для отладки.
1. Render Phase (Фаза рендера)
Это этап, на котором React вызывает ваши компоненты, строит новое дерево Fiber и вычисляет изменения (Reconciliation). Свойство фазы: Она асинхронная и прерываемая. React может начать рендер, прервать его ради более приоритетной задачи (например, ввода текста), отбросить результаты и начать заново. Именно поэтому в теле функционального компонента не должно быть сайд-эффектов (например, мутаций глобальных переменных) — функция компонента может быть вызвана несколько раз до того, как результат попадет на экран.
2. Commit Phase (Фаза фиксации)
Когда все изменения вычислены, React переходит ко второй фазе — применению патчей к реальному DOM.
Свойство фазы: Она синхронная и непрерываемая. Как только React начал изменять DOM, он не остановится, пока не закончит. Это гарантирует, что пользователь никогда не увидит наполовину обновленный интерфейс. Сразу после обновления DOM на этой фазе синхронно срабатывают хуки useLayoutEffect, а затем асинхронно — useEffect.
Итог
Virtual DOM и Reconciliation защищают нас от медленных операций браузера, минимизируя обращения к реальному DOM. Однако рост сложности приложений потребовал эволюции самого React. Переход от рекурсивного Stack Reconciler к связному списку Fiber позволил раздробить тяжелые вычисления и перестал блокировать Main Thread.
Но самое главное: архитектура Fiber, научившая React ставить рендер на паузу, стала фундаментом для следующего большого шага — Concurrent Mode (конкурентного режима) и управления приоритетами задач, что мы подробно разберем в следующей главе.