javascript

Как React обновляет UI: trigger → render → commit

  • воскресенье, 29 марта 2026 г. в 00:00:03
https://habr.com/ru/articles/1016180/

Когда говорят «React перерендерился» — обычно имеют в виду что-то расплывчатое. Новичкам это слово объясняет всё и ничего одновременно. В официальной документации процесс описан точнее: trigger → render → commit. Давайте разберём, что происходит на каждом этапе — без магии, зато с Fiber, флагами и браузерным пайплайном.


Render-фаза: вычисление изменений

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 и completeWork

beginWork — проход сверху вниз (top-down):

  • Проверяет, нужно ли вообще рендерить компонент. Если нет — срабатывает bailout, и ветка пропускается.

  • Если нужно — вызывает компонент, получает дочерние элементы.

  • Выставляет флаги: Placement, Update, Deletion, Passive, Layout и другие.

  • Рекурсивно спускается к детям.

completeWork — проход снизу вверх (bottom-up):

  • Финализирует узел, когда все его потомки уже обработаны.

  • «Всплывает» эффекты вверх через subtree flags.

Идея subtree flags простая: чтобы не обходить всё дерево целиком при каждом коммите, React поднимает к родителю информацию о том, есть ли вообще что-то важное в поддереве. Если нет — ветку можно быстро пропустить. Это делает commit-фазу значительно дешевле.

Флаги

Флаги — это метки на fiber-узлах, которые говорят React, что нужно сделать в commit-фазе:

Флаг

Значение

Placement

Вставить новый DOM-узел

Update

Обновить атрибуты или текст

Deletion

Удалить элемент

Ref

Работа с ref-ссылками

Layout

Запланировать useLayoutEffect

Passive

Запланировать useEffect

В render-фазе флаги планируются, в commit-фазе — исполняются.


Commit-фаза: применение изменений к DOM

Когда 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


Browser Pipeline (Этапы работы браузера): что происходит после commit фазы

После того как commit-фаза завершена, управление переходит к браузеру:

  1. Style recalculation — вычисляются финальные стили для каждого элемента.

  2. Layout / Reflow — вычисляется геометрия.

  3. Paint / Repaint — рисуются пиксели.

  4. Composite — слои объединяются в финальный кадр, часто с участием GPU.

Браузер умный: если изменение не требует пересчёта геометрии — reflow не будет. Если не менялась визуальная часть — paint пропустится.

useEffect и дискретные события

useEffect выполняется асинхронно — обычно после того, как браузер уже отрисовал кадр (после paint). Но есть исключение: если эффект был вызван дискретным событием (click, keydown, input, submit), React может выполнить его до paint — чтобы результат успел быть замечен системой событий. Ключевое слово: «может», не «гарантированно».

Дискретные события — это чётко ограниченные действия пользователя в конкретный момент времени. В отличие от непрерывных: scroll, mousemove, pointermove.


Concurrent режим

В Concurrent React render-фаза становится гибкой: она может быть разбита на части, прервана и возобновлена. Это нужно для поддержания актуальности интерфейса.

Пример: пользователь нажимает кнопку — запускается рендер. В процессе он начинает вводить текст. Ввод имеет более высокий приоритет. React прерывает текущий рендер и немедленно запускает новый, учитывающий введённый текст. Первое work-in-progress дерево становится устаревшим, React отбрасывает его. Старое дерево позже удалит сборщик мусора.

Commit-фаза при этом не меняется — она всегда одна и всегда синхронна.


Главное

  • Re-render ≠ обновление DOM. Рендер — вычисление следующего состояния UI. DOM трогается только в commit-фазе, и только если изменения реально есть.

  • Render-фаза может выполниться сотни раз, commit-фаза — всегда одна на обновление.

  • В Concurrent режиме render-фаза прерываема; commit-фаза — никогда.

  • useLayoutEffect запускается синхронно после мутации DOM, useEffect — асинхронно после paint (с оговоркой про дискретные события).