React
Reconciliation и Fiber
Список из тысячи комментариев в ленте обновляется при каждом новом ответе. Если бы React пересобирал весь DOM с нуля на каждое изменение, интерфейс бы заметно подтормаживал, а поля ввода теряли фокус. Вместо этого React сравнивает новое описание UI со старым и меняет в настоящем DOM только то, что реально поменялось. Этот алгоритм сравнения называется reconciliation, и в его основе с 2017 года (React 16) лежит структура Fiber, придуманная ради плавности под нагрузкой.
- Ленты соцсетей: вставка нового поста в начало списка не должна перерисовывать тысячи существующих узлов
- Таблицы и гриды: сортировка и фильтрация переставляют строки, и от корректной идентичности зависит сохранность состояния ячеек
- Чаты: приход сообщения добавляет один узел, а не пересоздаёт весь список переписки
- Конкурентный рендеринг: React 18+ прерывает долгий рендер ради отклика на ввод, и это возможно именно благодаря Fiber
- DevTools Performance Tracks (React 19.2): профайлер показывает фазы reconciliation и коммита прямо в браузере
Предварительные знания
- Модель рендера: компонент возвращает описание UI (дерево элементов), а не сам DOM
- Virtual DOM как промежуточное лёгкое дерево описаний
- Базовое понимание дерева DOM: узлы, их типы и вложенность
Зачем переписали движок в Fiber
До React 16 reconciliation работал рекурсивно и синхронно: начав обходить дерево, React не мог остановиться, пока не дойдёт до конца. На больших деревьях это блокировало главный поток, и ввод пользователя подвисал. Эндрю Кларк, Себастьян Маркбоге и команда несколько лет проектировали новый движок под названием Fiber и выпустили его в React 16 (сентябрь 2017). Главная идея: представить работу как связный список единиц-fiber, который можно обходить по кусочкам, ставить на паузу, возобновлять и приоритизировать. Это заложило фундамент для конкурентного рендеринга, появившегося позже в React 18.
Сравнение по типу и позиции
После каждого рендера React получает новое дерево элементов и сравнивает его с предыдущим. Полное сравнение двух деревьев в общем случае слишком дорого, поэтому React использует две эвристики. Первая: элементы сопоставляются по их позиции в дереве - первый ребёнок к первому, второй ко второму. Вторая: на каждой позиции React смотрит на тип элемента. Если тип совпал - узел переиспользуется и обновляются только изменившиеся props. Если тип изменился - всё поддерево на этой позиции пересоздаётся заново.
Отсюда практическое следствие, которое часто удивляет: состояние компонента привязано не к самому компоненту, а к его позиции в дереве. Если на одной позиции остаётся элемент того же типа, его состояние переживает рендер, даже если props сменились полностью. Если же тип на позиции меняется, состояние пропадает вместе со старым поддеревом.
Сравнение по позиции - это компромисс. Точный алгоритм сравнения произвольных деревьев имеет кубическую сложность от числа узлов, что неприемлемо для UI. Эвристики 'тип плюс позиция' дают линейную сложность ценой одного допущения: на одной позиции обычно стоит элемент того же назначения. Для списков это допущение ломается, и тогда нужны keys.
На одной и той же позиции в дереве React видит элемент другого типа, чем на прошлом рендере. Что он сделает?
Дерево Fiber и прерываемая работа
Fiber - это внутренняя структура данных React, появившаяся в React 16. Каждому элементу UI соответствует объект-fiber, и эти объекты связаны в обходимую структуру: ребёнок, следующий брат, родитель. Главное отличие от старого рекурсивного движка в том, что обход Fiber-дерева можно остановить после любой единицы работы, отдать управление браузеру и продолжить позже. Рекурсию так не прервёшь, а связный список fiber - можно.
Работа делится на две фазы. Фаза render строит новое Fiber-дерево и вычисляет дифф - её можно прерывать, ставить на паузу и даже отбрасывать, потому что она не трогает настоящий DOM. Фаза commit применяет накопленные правки к DOM за один синхронный проход - её прервать нельзя, иначе пользователь увидит полуобновлённый интерфейс. Прерываемость фазы render и есть фундамент конкурентного рендеринга в React 18+.
Прерываемость нужна для отзывчивости. Если идёт долгий рендер большого списка, а пользователь печатает в поле, React может приостановить рендер, обработать ввод и вернуться к списку. Без Fiber главный поток был бы занят целиком, и ввод подвисал бы до конца рендера. На этом построены useTransition и автоматическая приоритизация обновлений.
Какое ключевое свойство дала архитектура Fiber по сравнению со старым рекурсивным движком?
Почему спискам нужны keys
Сопоставление по позиции отлично работает для статичной структуры, но ломается на динамических списках. Если в начало списка вставить новый элемент, все последующие сдвинутся на одну позицию. По позиционной эвристике React решит, что на каждой позиции изменился контент, и будет обновлять не те узлы, вместо того чтобы просто вставить один новый в начало. Чтобы этого избежать, React нужна стабильная идентичность каждого элемента - её и задаёт проп key.
Key - это не просто способ убрать предупреждение в консоли. Это обещание React: 'элемент с таким key - это тот же самый элемент между рендерами, где бы он ни оказался в списке'. Благодаря key React сопоставляет элементы по идентичности, а не по позиции: при вставке в начало он понимает, что старые элементы те же, лишь сдвинулись, и достаточно создать один новый узел. Состояние и DOM существующих элементов сохраняются.
Key должен быть стабильным и уникальным среди соседей: id из данных, а не индекс массива и не случайное число. Индекс как key привязывает идентичность к позиции, а не к данным - ровно та проблема, от которой key должен спасать. Случайный key на каждый рендер заставит React пересоздавать все элементы заново. Детальный разбор этих ловушек - в следующем уроке.
Зачем React нужен проп key в списках?
Связь с другими темами
Урок про движок сравнения деревьев. Рядом:
- Модель рендера — Reconciliation - шаг, который превращает результат рендера в минимальные правки реального DOM
- Keys вглубь — Keys задают идентичность элементов списка, на которую опирается сопоставление
- React Compiler — Компилятор сокращает работу до reconciliation, мемоизируя вычисления на рендере
Итог
- Reconciliation - это алгоритм, которым React сравнивает новое дерево элементов со старым и вычисляет минимальный набор правок реального DOM
- Сопоставление идёт по типу элемента и позиции в дереве: тот же тип на той же позиции - React обновляет существующий узел и сохраняет его состояние
- Разный тип на той же позиции - React удаляет старое поддерево целиком и строит новое с нуля, теряя состояние
- Fiber (React 16, 2017) - структура, представляющая работу как обходимый по частям список, что позволяет ставить рендер на паузу, возобновлять и приоритизировать
- В списках позиции недостаточно: при перестановке или вставке нужны keys, чтобы React понимал, какой элемент какому соответствует
Связанные уроки
- rc-10-render-mental-model — Reconciliation - это и есть механизм, который превращает описание UI из рендера в точечные правки DOM
- rc-18-keys-deep — Поняв сопоставление по позиции, дальше разбираем keys как явную идентичность элементов в списках
- rc-20-react-compiler — Компилятор оптимизирует то, что происходит до reconciliation - вычисление и сравнение деревьев на рендере