awesome-everything EN
↑ Обратно к восхождению

Браузер и фронтенд-рантайм

Чистота фазы render и подшаги фазы commit

Суть Render должен быть чистым, потому что React 18 может его переиграть; commit синхронен, атомарен и имеет три подшага — before-mutation, mutation и layout — каждый со своими API и режимами отказа.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 13 min

Компонент, записывающий что-то в глобальный счётчик при каждом рендере, прекрасно работает в React 17. В React 18 в StrictMode этот счётчик удваивается. Разница — не баг: React сообщает вам, что фаза render может переиграть вашу функцию, и побочные эффекты там выполнятся более одного раза.

Фаза render: прерываема, должна быть чистой. Фаза render обходит дерево workInProgress, вызывает каждую функцию-компонент и выполняет реконсиляцию. Так как React 18 может приостановить эту фазу, возобновить или выбросить и начать заново, ваша функция-компонент может быть вызвана более одного раза для одного видимого обновления — и приостановленный рендер может висеть незавершённым, пока пользователь взаимодействует. Именно поэтому render должен быть чистым: никаких побочных эффектов, мутаций внешних переменных, чтения или записи DOM, сетевых запросов в теле функции. Побочный эффект в render выполняется непредсказуемое число раз. StrictMode React намеренно двойной-вызывает компоненты в режиме разработки именно для раннего обнаружения нечистых рендеров. Правильное место для побочных эффектов — useEffect (после commit) или обработчик события (вне render).

Фаза commit: атомарна, синхронна, три подшага. Как только дерево workInProgress полностью построено, commit выполняется одним непрерываемым блоком. Три подшага:

  1. Before-mutation: React читает любое состояние до мутации. Здесь происходит getSnapshotBeforeUpdate и снимок очистки для useLayoutEffect.
  2. Mutation: React применяет каждое изменение DOM — вставки, удаления, обновления атрибутов, изменения текста — на основе флагов эффектов, проставленных на fiber-ах во время render.
  3. Layout: React запускает коллбэки useLayoutEffect и обновляет рефы синхронно, до того как браузер рисует.

Так как commit синхронен и непрерываем, огромное дерево с тысячами мутаций производит одну длинную задачу — commit — это часть React, которую нельзя нарезать на слайсы, поэтому способ держать его коротким — держать число мутаций небольшим (виртуализация, мемоизация).

Временная шкала фазы commit
before-mutationgetSnapshotBeforeUpdate · снимок очистки useLayoutEffect
mutationВставки · удаления · обновления атрибутов · изменения текста
layoutuseLayoutEffect · обновления рефов → до отрисовки браузером
после отрисовкиuseEffect (пассивный) → отдельная асинхронная задача

useLayoutEffect vs useEffect. Два хука отличаются тем, когда они выполняются относительно отрисовки. useLayoutEffect запускается синхронно на шаге layout фазы commit — после мутации DOM, до отрисовки браузером. useEffect («пассивный эффект») запускается асинхронно после отрисовки, в более поздней задаче. Правило: всё, что должно выполниться до того, как пользователь увидит кадр, — измерение только что обновлённого DOM-узла и корректировка вёрстки для предотвращения видимой вспышки — относится к useLayoutEffect. Всё остальное — запросы данных, подписки, логирование — относится к useEffect, потому что делать это в useLayoutEffect задерживает отрисовку. Злоупотребление useLayoutEffect — реальная регрессия INP: каждый layout-эффект — синхронная работа фазы commit, откладывающая отрисовку.

Частая ошибка

useLayoutEffect, который читает getBoundingClientRect, а затем пишет стиль, вызывает синхронный layout внутри commit — принудительный reflow. Браузер должен пересчитать layout в середине commit, чтобы ответить на вызов getBoundingClientRect, затем React записывает стиль, затем браузер должен пересчитать layout ещё раз до отрисовки. Это аналог layout thrash для фазы commit, невидимый в обычных DevTools, если не смотреть на тайминг commit.

Викторина

Почему тело функции-компонента должно быть чистым (без побочных эффектов)?

Расставь шаги по порядку

Расставьте подшаги фазы commit по порядку — с момента завершения дерева workInProgress.

  1. 1 Before-mutation: чтение состояния до мутации (getSnapshotBeforeUpdate)
  2. 2 Mutation: применение вставок, удалений, обновлений DOM
  3. 3 Swap: дерево workInProgress становится деревом current
  4. 4 Layout: запуск useLayoutEffect, обновление рефов (до отрисовки)
  5. 5 Браузер рисует; пассивные эффекты (useEffect) запускаются после
Викторина

Нужно измерить размер DOM-узла сразу после обновления React и до того, как пользователь увидит кадр. Какой API использовать?

Вспомните перед уходом
  1. 01
    Почему React StrictMode двойной-вызывает функции компонентов в режиме разработки?
  2. 02
    Перечислите три подшага фазы commit по порядку.
  3. 03
    Какой режим отказа у злоупотребления useLayoutEffect?
Итог

Фаза render вызывает ваши функции-компоненты и в конкурентном режиме React 18 может сделать это несколько раз за одно видимое обновление — поэтому тело функции должно быть чистым. Любой побочный эффект там (запись в глобальный счётчик, мутация рефа, запрос данных) выполняется непредсказуемое число раз. Фаза commit — полная противоположность: один синхронный, непрерываемый блок с тремя упорядоченными шагами. Before-mutation читает снимки. Mutation применяет каждое изменение DOM, помеченное React во время render. Layout запускает useLayoutEffect и обновляет рефы до отрисовки браузером. Пассивные useEffect-хуки выполняются отдельно и срабатывают после отрисовки. Фаза commit нельзя нарезать на слайсы, поэтому держать число мутаций небольшим — главный рычаг производительности commit.

Связанные уроки
встречается в143
Продолжить восхождение ↑Реконсиляция: эвристики диффа и ловушка ключей
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.