Skip to content

Instantly share code, notes, and snippets.

@kinda-neat
Last active September 5, 2024 06:00
Show Gist options
  • Save kinda-neat/1d65872a5d2c2bf257bbc61c113039a7 to your computer and use it in GitHub Desktop.
Save kinda-neat/1d65872a5d2c2bf257bbc61c113039a7 to your computer and use it in GitHub Desktop.
Уверены что понимаете как работает useLayoutEffect?

В доке по useLayoutEffect можно найти следующие отрывки:

  • React guarantees that the code inside useLayoutEffect and any state updates scheduled inside it will be processed before the browser repaints the screen.
  • Call useLayoutEffect perform the layout measurements before the browser repaints the screen:
  • The code inside useLayoutEffect and all state updates scheduled from it block the browser from repainting the screen. When used excessively, this makes your app slow. When possible, prefer useEffect.

Как это возможно? Как Реакт может гарантировать что код внутри useLayoutEffect и стейт апдейты запланированные внутри него будут выполнены до перерисовки экрана браузером? Как Реакт может влиять на перерисовку браузером, как может ее блокировать? Как Реакт это делает и как вы сами бы могли бы сделать что-то подобное? Какое место занимает/когда выполняется useLayoutEffect? Что такого Layout в useLayoutEffect? Как может вызывать проблемы с производительностью?

Здесь можно пойти и подумать самим, попробовать объяснить себе, проговорить вслух как на собесе :) И затем вернуться позже, ну либо продолжить читать сразу.


Отправная точка размышлений начинается тут

Всё упирается в то что у нас один тред исполнения - the main/UI thread. Эмпирически и теоритически можно узнать что на мейн треде происходят следующие интересующие нас работы:

  • клиентский, нами написанный js код
  • Layout/Reflows это значит что браузер (рас/пере)считывает геометрию элементов, где кто и как расположен и т.п. здесь же на мейн треде
  • Painting - рисовка картинки которую юзер видит на экране

Один тред исполнения говорит о том, что в один момент времени может выполняться только одна из этих работ, мейн тред либо выполняет клиентскую логику, либо делает layout/reflows, либо рисует результат на экран.

Мы пишем клиентский код с помощью Реакта и определяем то что будет влиять на перерисовку экрана - интеракции для пользователя и что на них происходит, подключаемся к каким-то внешним источникам данных и т.п. Когда происходят интеракции и в результате меняется стейт, например, Реакт занимает мейн тред и выполняет свою работу (пройди по дереву и узнай что должно быть отрисовано и какие эффекты должны произойти, пойми какие изменения нужно внести в DOM, внеси их, выполни эффекты).

И вот здесь ключ - Реакт занимает мейн тред. Если Реакт его занял то никакая другая работа не может выполняться по определению (мейн треда), в том числе отрисовка браузером. Отсюда, как можно гарантировать что какая-то работа будет выполнена до перерисовки экрана браузером/блокировать перерисовку экрана? Да просто продолжать занимать мейн тред и всё, не освобождать его! Это то что и делает Реакт.

Как это конкретно связано с useLayoutEffect? При возникновении определенных событий Реакт делает ререндеринг - проходит по интересующим частям дерева и собирает:

  1. описание того что должно быть отрисовано (JSX)
  2. описание того какие эффекты должны быть выполнены

Существует два типа эффектов: useEffect и useLayoutEffect.

В случае useEffect'а флоу такой: пойми какие изменения нужно внести в DOM -> внеси их -> запланируй выполнение эффектов асинхронно, чтобы они были выполнены на мейн-треде когда-нибудь потом, в будущем -> освободи мейн тред (и в результате позволь браузеру отрисовать что ты изменил в DOM).

В случае useLayoutEffect'a флоу такой: пойми какие изменения нужно внести в DOM -> внеси их -> если есть какие-либо useLayoutEffect'ы выполни их здесь же, синхронно, на месте, "в этом же колстэке", в этом же чанке работы на мейн-треде -> (сделай доп ререндеринг) -> запланируй выполнение остальных эффектов асинхронно, чтобы они были выполнены на мейн-треде когда-нибудь потом, в будущем -> освободи мейн тред (и в результате позволь браузеру отрисовать что ты изменил в DOM).

useEffect-vs-useLayoutEffect

Окей, что такого Layout в useLayoutEffect?

Здесь стоит знать про пайплайн рендеринга браузера. Нас интересуют два этапа: Layout и Paint и те ситуации в которых у нас будет работа по Layout.

Такая работа может быть вызвана разными факторами, два основных:

  • при чтении информации о размерах, через getBoundingClientRect(), например
  • при манипуляциях с DOM'ом - удаляем/добавляем ноды в какие-то узлы DOM'а, например

В интересующих нас ситуациях, прежде чем что-либо отрисовывать (Paint), браузеру нужно будет выполнить работу по перерасчету Layout'a. Эта работа может быть вызвана либо выхлопом Реакта (коммит в дом), либо нашим кодом (чтение геометрии элементов, тригер доп рендерингов в результате которых будет больше коммитов в дом). Вместе с коммитом в дом обновится информация о геометрии элементов которую мы и можем прочитать, после коммита реакт и запустит часть работы с useLayoutEffect'ами.

Мне нравится думать про этап с реактом занимающим мейн тред как про этап с редактированием DOM/Render tree - описания того что мы хотим чтобы браузер отрисовал на экране, и это логично что мы можем менять описание сколь угодно раз, в каком угодно порядке. Пока Реакт не освободил мейн тред, работы по Layout'у может быть сколько угодно. Как только Реакт осободит мейн тред, браузер получит возможность сделать Paint, тогда и изменения внесенные в DOM отобразятся на экране. Чтобы юзер увидел изменения, браузер должен нарисовать фрейм/картинку на экране по render tree, и пока мы не дали браузеру возможность рисовать, юзер ничего не увидит.

Где всё это может быть полезно? Как раз в ситуациях где для выполнения интеракции/логики нужно знать геометрические размеры элемента/мира вокруг, практические примеры этого:

Разберем на примере раскрытия списка с динамическим количеством элементов внутри. Список элементов в корзине например. Что дано:

  • высоту можно анимировать только из фиксированных, определенных размеров - в пикселях например.
  • список - элемент с динамической высотой, количество элементов в корзине варьируется === высота списка тоже (а нам для анимации раскрытия нужна фиксированная, ее мы и получим с помощью useLayoutEffect!). Тоже самое кстати в случае с тултипом: в общем случае, размер/высота тултипа и расстояния до границ вьюпорта тоже могут быть динамическими (юзер может остановиться скроллить где угодно, контент тултипа может быть сколь угодно большим/маленьким). Поэтому нам нужна самая актуальная/свежая высота/расстояние до границ вьюпорта на момент интеракции (клик на закрыть/открыть кнопку, при наведении на элемент.)

Я написал хук для анимации высоты, разберем что там происходит:

  • юзер кликает на открыть, в результате isExpanded переходит в true
  • по логике компонента при isExpanded = true возвращает JSX в котором блок с текстом должен быть показан на экране, это он и коммитит в реальный DOM
  • сейчас блок с текстом в доме, с актуальными геометрическими размерами. Ок, размеры есть, DOM поменялся, но браузер не может это отрисовать на данный момент - мейн тред занят Реактом и будет занят дальше для выполнения useLayoutEffect'ов
  • и тут в игру вступает логика обернутая в useLayoutEffect'ы
  • в useLayoutEffect я сохраняю высоту элемента в локальный стейт с помощью рефки на элемент в ДОМе и здесь же перевожу isExpanded в false, тем самым в JSX блок с текстом должен быть скрыт - возвращаю к состоянию до клика раскрытия списка. После этого асинхронно планирую анимацию в очередной раз - теперь у нас есть вся информация для выполнения интеракции (анимации в данном случае) - мы знаем точную высоту в которую нам нужно будет санимировать переход
  • колбэк в rAF выполняется asap, в котором тритерится запрошенная юзером анимация
  • происходит анимация - на экране видим то что и задумывалось.

Ух, тяжело класть мысли в слова!

Еще способ продемонстрировать где происходит useLayoutEffect - через порядок шагов:

  • тригер интеракции (раскрыть список/блок с текстом)
  • рендеринг
  • коммит в дом (вместе с коммитом в DOM'е обновится информация о геометрии элементов)
  • use layout effec’ы (продолжаем занимать мейн тред, запускаем layout эффекты, читаем через рефки геометрию, сохраняем и т.п.)
  • рендеринг (используя useState сохранили высоту, поменялся стейт - в ответ произошел ререндеринг)
  • коммит в дом
  • React освобождает мейн тред
  • браузер рисует картинку на основе того что в доме/render tree. И только здесь юзер увидит итоговый результат, всё до этого было без визуальных изменений - js логика, манипуляции над стркутрами данных (DOM, etc.)
Подробнее про то, как это возможно? Знать геометрию без/до рисовки на экране.

Этапы в пайплайне рендеринга происходят последовательно, в сторогом порядке. Почему? Потому что для того чтобы выполнить следующий шаг нужно знать результат предыдущего. Например, чтобы что-то нарисовать (Paint), нужно знать что нарисовать (Render Tree - результат расчета Style/Layout), логично!

Отсюда если у нас есть работа по расчету геометрии (Layout), то она будет выполнена до рисовки. Как было сказано выше, работа по Layout может выполняться сколько угодно раз пока код в котором эта работа происходит не освободит мейн тред. Какая работа по Layout была выполнена в хуке с анимированным раскрытием в разные моменты времени одним и тем же чанком работы на мейн треде?

  • в DOM добавлен блок с текстом (в результате рендеринга)
  • чтение размеров через getBoundingClientRect (код внутри useLayoutEffect)
  • в DOM'е скрыт блок с текстом (в результате ререндеринга вызванного setState внутри useLayoutEffect)

Итого три раза мы манипулировали содержимым ДОМа что выражалось в разной геометрии на каждом из этапов. Таким образом вы можете сколь угодно и как угодно играться с геометрией без того чтобы юзер видел это на экране. Результат на экране появится только тогда, когда мейн тред будет освобожден для рисовки браузером.

height-animation

Как может вызывать проблемы с производительностью?

Логика в useLayoutEffect/рендеринги тритернутые логикой внутри задерживают Реакт на мейн треде, чем больше и дольше подобной логики нужно выполнить на мейн треде тем дольше браузер не может отвечать на интеракции пользователя (скроллинг, клики, заполнение инпутов и т.п.), т.к. не может рисовать фреймы. Отсюда сайт становится неотзывчивым/неинтерактивным, юзер кликает и ничего не происходит, либо происходит с задержкой и уже не тогда когда нужно.

Еще заметки по другим темам здесь.

Источники:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment