Как React обновляет UI: trigger → render → commit
- воскресенье, 29 марта 2026 г. в 00:00:03
Когда говорят «React перерендерился» — обычно имеют в виду что-то расплывчатое. Новичкам это слово объясняет всё и ничего одновременно. В официальной документации процесс описан точнее: trigger → render → commit. Давайте разберём, что происходит на каждом этапе — без магии, зато с Fiber, флагами и браузерным пайплайном.
Render-фаза — это не обновление DOM. Это вычисление того, каким DOM должен стать.
React рекурсивно обходит дерево компонентов, вызывает их функции (или render() у классовых компонентов) и получает JSX. JSX трансформируется в React-элементы — лёгкие иммутабельные JS-объекты, описывающие UI. На их основе Fiber строит work-in-progress дерево — полностью виртуальную структуру, существующую только в памяти.
Важный момент: work-in-progress дерево строится на основе текущего Fiber-дерева, а не реального DOM. DOM и Fiber — разные структуры. Fiber — это абстракция над DOM, не его отражение.
Параллельно с построением дерева происходит reconciliation: новые элементы через ссылку alternate сравниваются со старыми с помощью diff-алгоритма. Найденные изменения помечаются флагами. Именно поэтому стабильные ключи (key) так важны — без них React не может корректно сопоставить элементы.
Render-фаза состоит из двух этапов.
beginWork — проход сверху вниз (top-down):
Проверяет, нужно ли вообще рендерить компонент. Если нет — срабатывает bailout, и ветка пропускается.
Если нужно — вызывает компонент, получает дочерние элементы.
Выставляет флаги: Placement, Update, Deletion, Passive, Layout и другие.
Рекурсивно спускается к детям.
completeWork — проход снизу вверх (bottom-up):
Финализирует узел, когда все его потомки уже обработаны.
«Всплывает» эффекты вверх через subtree flags.
Идея subtree flags простая: чтобы не обходить всё дерево целиком при каждом коммите, React поднимает к родителю информацию о том, есть ли вообще что-то важное в поддереве. Если нет — ветку можно быстро пропустить. Это делает commit-фазу значительно дешевле.
Флаги — это метки на fiber-узлах, которые говорят React, что нужно сделать в commit-фазе:
Флаг | Значение |
|---|---|
| Вставить новый DOM-узел |
| Обновить атрибуты или текст |
| Удалить элемент |
| Работа с ref-ссылками |
| Запланировать |
| Запланировать |
В render-фазе флаги планируются, в commit-фазе — исполняются.
Когда work-in-progress дерево построено и все изменения помечены, начинается commit-фаза. Она синхронная и неделимая — в отличие от render-фазы, прервать её нельзя. Если это сделать, экран окажется в несогласованном состоянии.
Commit-фаза состоит из трёх этапов.
beforeMutation — момент перед изменением DOM:
Читает флаг Snapshot → вызывает getSnapshotBeforeUpdate у классовых компонентов.
Очищает старые ref-ссылки (ref.current = null).
mutation — собственно изменение DOM:
Placement → вставляет узел
Update → меняет атрибуты, текст
Deletion → удаляет элемент
Именно здесь реальный DOM меняется.
layout — после мутации, но до того как браузер отрисовал кадр:
Запускает componentDidMount / componentDidUpdate
Синхронно запускает useLayoutEffect
Обновляет ref-ссылки новыми значениями
Планирует (но не запускает!) useEffect
После того как commit-фаза завершена, управление переходит к браузеру:
Style recalculation — вычисляются финальные стили для каждого элемента.
Layout / Reflow — вычисляется геометрия.
Paint / Repaint — рисуются пиксели.
Composite — слои объединяются в финальный кадр, часто с участием GPU.
Браузер умный: если изменение не требует пересчёта геометрии — reflow не будет. Если не менялась визуальная часть — paint пропустится.
useEffect выполняется асинхронно — обычно после того, как браузер уже отрисовал кадр (после paint). Но есть исключение: если эффект был вызван дискретным событием (click, keydown, input, submit), React может выполнить его до paint — чтобы результат успел быть замечен системой событий. Ключевое слово: «может», не «гарантированно».
Дискретные события — это чётко ограниченные действия пользователя в конкретный момент времени. В отличие от непрерывных: scroll, mousemove, pointermove.
В Concurrent React render-фаза становится гибкой: она может быть разбита на части, прервана и возобновлена. Это нужно для поддержания актуальности интерфейса.
Пример: пользователь нажимает кнопку — запускается рендер. В процессе он начинает вводить текст. Ввод имеет более высокий приоритет. React прерывает текущий рендер и немедленно запускает новый, учитывающий введённый текст. Первое work-in-progress дерево становится устаревшим, React отбрасывает его. Старое дерево позже удалит сборщик мусора.
Commit-фаза при этом не меняется — она всегда одна и всегда синхронна.
Re-render ≠ обновление DOM. Рендер — вычисление следующего состояния UI. DOM трогается только в commit-фазе, и только если изменения реально есть.
Render-фаза может выполниться сотни раз, commit-фаза — всегда одна на обновление.
В Concurrent режиме render-фаза прерываема; commit-фаза — никогда.
useLayoutEffect запускается синхронно после мутации DOM, useEffect — асинхронно после paint (с оговоркой про дискретные события).