Ви коли-небудь створювали підказку (tooltip), яка різко перестрибує з верхнього лівого кута на своє правильне місце? Або модальне вікно, яке на мить з'являється неправильного розміру, перш ніж стабілізуватися? Цей миттєвий глюк — це мерехтіння макета (layout flicker). Він виникає, коли React зчитує DOM, обчислює виправлення та оновлює стан, але браузер уже почав виводити пікселі на екран. Звичним рішенням є заміна useEffect на useLayoutEffect. Ця заміна працює, але лише якщо ви точно розумієте, коли саме кожен хук спрацьовує в конвеєрі браузера.
Конвеєр браузера: Render, Commit, Paint
React оновлює компонент у три різні етапи. На фазі render React будує — або перебудовує — Virtual DOM і обчислює різницю (diff). Жодних реальних змін пікселів ще немає; це суто обчислення, що відбуваються в пам'яті. Далі йде фаза commit, де React застосовує ці зміни до реальних вузлів DOM. Оновлюються стилі, вузли додаються або видаляються, змінюється текст.
Потім у справу береться браузер. На фазі paint рушій рендерингу браузера обчислює геометрію макета та малює пікселі на екрані. Ця послідовність є жорсткою. Браузер має завершити layout, перш ніж зможе виконати paint, і він має закінчити paint, перш ніж користувач побачить щось нове. Розрив між commit і paint вимірюється мілісекундами, але він реальний, і це саме те вікно, де useEffect та useLayoutEffect розходяться.
Чому useEffect спричиняє мерехтіння
useEffect виконується асинхронно, за розкладом після того, як браузер уже виконав paint екрана. DOM оновлено, пікселі намальовані, і лише потім React повертається, щоб запустити ваш ефект.
Уявіть, що ви рендерите випадаюче меню під кнопкою. Всередині useEffect ви викликаєте buttonRef.current.getBoundingClientRect(), обчислюєте правильні координати top та left і зберігаєте їх у стані. Оскільки useEffect запускається після paint, браузер уже намалював випадаюче меню в його стандартному положенні, можливо, top: 0, left: 0. Тільки після цього paint ваш ефект оновлює стан. React виконує commit виправлених координат, і браузер знову виконує paint. Користувач бачить два кадри: неправильне положення, а потім правильне. Це візуальне перестрибування і є тим самим мерехтінням, якого всі намагаються уникнути.
Для отримання даних, викликів API, відстеження аналітики або налаштування слухачів подій ця затримка не має значення. Користувачеві байдуже, чи спрацює аналітичний маяк через кілька мілісекунд після paint. Насправді, перенесення невізуальної роботи на період після paint допомагає підтримувати швидкість початкового рендерингу. Але для виправлень, що залежать від макета, useEffect просто занадто пізній.
Як useLayoutEffect блокує Paint
useLayoutEffect виконується синхронно, одразу після того, як React змінює DOM, але до того, як браузер отримає можливість обчислити layout або намалювати пікселі. Він повністю блокує конвеєр paint.
Якщо ви виконаєте те саме вимірювання випадаючого меню всередині useLayoutEffect, послідовність зміниться. React виконує commit початкового оновлення DOM, запускає ваш layout effect, і оновлення стану викликає синхронний ререндер. React виконує commit виправлених координат, і лише після цього браузер виконує paint. Користувач бачить один кадр, який уже є правильним.
Така блокуюча поведінка є водночас і особливістю, і ризиком. Оскільки useLayoutEffect не дозволяє браузеру виконувати paint, поки він не завершить роботу, будь-які важкі обчислення всередині нього заморожують інтерфейс. Навіть кілька десятків мілісекунд заблокованого paint відчуваються користувачем як смикання (jank). Ось чому в документації React прямо радять починати з useEffect і переходити на useLayoutEffect лише тоді, коли ви фактично помічаєте мерехтіння, яке не можете ігнорувати.
Коли використовувати кожен хук
Більша частина вашої логіки має належати до useEffect. Використовуйте його для:
- Отримання даних з API
- Налаштування підписок або слухачів подій
- Надсилання аналітичних подій
- Будь-якого побічного ефекту, який не зчитує та не змінює макет негайно
Залиште useLayoutEffect для операцій, які повинні зчитувати DOM і записувати дані назад до того, як користувач побачить кадр:
- Вимірювання розмірів елементів, таких як ширина, висота або позиція прокрутки
- Обчислення координат для підказок (tooltips), поповерів (popovers) або контекстних меню
- Запобігання видимим зсувам макета, коли візуальне положення залежить від відрендереної геометрії
Якщо ви не впевнені, що обрати, за замовчуванням використовуйте useEffect. Переходьте на useLayoutEffect лише тоді, коли помітите візуальну нестабільність. Це одне лише правило дозволить переважній більшості React-додатків працювати плавно.
Підступність серверного рендерингу (SSR)
Якщо ви використовуєте Next.js, Remix або будь-який фреймворк, який рендерить React на сервері, ви отримаєте попередження щодо useLayoutEffect. Оскільки на сервері немає DOM, хуку немає чого вимірювати. React попереджає вас, що він очікував середовище браузера, але не знайшов його. Під час гідратації ця невідповідність також може спричинити приховані помилки, оскільки розмітка, відрендерена на сервері, та перший запланований рендер на клієнті можуть відрізнятися.
Стандартне рішення — це ізоморфний хук, який обирає правильний ефект залежно від середовища:
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
Використовуйте цю обгортку в будь-якому компоненті, якому потрібно вимірювати DOM-вузли, але який може виконуватися під час серверного рендерингу. Вона прибирає попередження та забезпечує стабільність вашого серверного виводу.
Продуктивність та найкращі практики
Оскільки useLayoutEffect блокує відмальовування, намагайтеся робити тіло хука якомога легшим. Зчитайте значення макета, обчисліть корекцію та запишіть її назад. Не виконуйте запити даних, парсинг великих об'єктів або запуск важких алгоритмів всередині нього. Важкий код тут заблокує основний потік і призведе до того, що ваш інтерфейс здаватиметься «завислим».
Коли ви вимірюєте елементи, використовуйте React refs замість document.getElementById. Refs прив'язані до екземпляра вашого компонента, зберігаються під час ререндерів без жодних хитрощів із запитами та надійно працюють із порталами або умовним рендерингом. Глобальний пошук за ID порушує інкапсуляцію компонента і може повернути null саме в той момент, коли він вам потрібен.
useEffect — це правильний варіант за замовчуванням майже для будь-якого побічного ефекту. Він дозволяє браузеру відмальовувати вміст без переривань і чисто обробляє дані, події та зовнішню синхронізацію. useLayoutEffect — це спеціалізований інструмент для конкретної задачі: зчитування макета та запису змін перед відмальовуванням. Опануйте різницю в часі їх виконання, і ви перестанете боротися з мерехтінням, а почнете його запобігати.
Головний висновок: Починайте з useEffect для всього. Як тільки ви побачите, що підказка або модальне вікно на мить з'являється не в тому місці, а потім виправляєсь — це ваш сигнал. Переходьте на useLayoutEffect, виміряйте DOM, скоригуйте макет і дозвольте браузеру відмалювати все один раз — правильно.
