Яка різниця між useEffect i useLayoutEffect?
useEffect і useLayoutEffect - різниця лише в таймінгу: один запускається після того, як браузер намалював екран, інший блокує малювання до завершення.
Теорія
TL;DR
useEffectспрацьовує після того, як браузер відмалював екран (асинхронно, після paint)useLayoutEffectспрацьовує до paint, блокуючи браузер до завершення колбека (синхронно, до paint)- Аналогія:
useEffect- це прибиральники, що приходять вже після того, як гості побачили безлад.useLayoutEffectприбирає до того, як гості зайшли - Потрібні DOM-вимірювання або виправлення без мерехтіння?
useLayoutEffect. Все інше?useEffect useLayoutEffectвидає попередження в SSR (Node.js);useEffectні
Швидкий приклад
function Box() {
const [count, setCount] = useState(0);
const ref = useRef();
// Запускається ПІСЛЯ paint - юзер спочатку бачить стару ширину, потім стрибок
useEffect(() => {
ref.current.style.width = `${count * 20}px`;
}, [count]);
// Заміни на це - ширина оновиться до paint, без мерехтіння
// useLayoutEffect(() => {
// ref.current.style.width = `${count * 20}px`;
// }, [count]);
return (
<div ref={ref} style={{ background: '#eee' }} onClick={() => setCount(c => c + 1)}>
Клік: {count}
</div>
);
}З useEffect браузер спочатку малює стару ширину, потім ефект спрацьовує і елемент стрибає. Заміна на useLayoutEffect виправляє ширину ще до того, як браузер малює перший піксель.
Головна різниця
Обидва хуки мають однаковий синтаксис, cleanup-функція працює так само. Відрізняється тільки момент запуску всередині фази commit у React. useLayoutEffect запускається синхронно після мутацій DOM, але ще до paint браузера. useEffect ставиться в чергу після paint з нижчим пріоритетом. Різниця в мілісекундах мала, але для ока помітна, коли потрібні DOM-вимірювання або виправлення стилів. Я бачив таку проблему в продакшені: тултіп, позиція якого вираховувалась у useEffect, помітно зіскакував на місце при кожному рендері.
Коли що використовувати
- Вимірювання DOM (ширина, висота, scroll position) перед рендером →
useLayoutEffect - Виправлення макету щоб уникнути видимого стрибка або мерехтіння →
useLayoutEffect - Анімації, що читають layout перед записом значень →
useLayoutEffect - Запити до API, таймери, підписки на події →
useEffect - Все, що не стосується видимого DOM-макету →
useEffect
Таблиця порівняння
| Аспект | useEffect | useLayoutEffect |
|---|---|---|
| Таймінг | Після paint (асинхронно) | До paint (синхронно) |
| Блокує paint браузера? | Ні | Так |
| Читання DOM-layout | Викликає мерехтіння | Безпечно, відповідає реальному стану |
| Ризик для продуктивності | Мінімальний | Може затримати рендер якщо колбек важкий |
| SSR (Node.js) | Безпечно | Видає попередження |
| Типове застосування | API, підписки, таймери | DOM-вимірювання, виправлення стилів |
Як це працює всередині
Під час фази commit у React колбеки useLayoutEffect запускаються синхронно відразу після мутацій DOM, до paint браузера. Колбеки useEffect ставляться в чергу після paint через внутрішній React Scheduler з нижчим пріоритетом. У React 18 з concurrent mode useLayoutEffect все одно залишається синхронним і блокує transitions. Для несрочної роботи всередині startTransition використовуй useEffect.
Типові помилки
Виправлення scroll у useEffect:
// Неправильно: юзер спочатку бачить верх сторінки, потім стрибок
useEffect(() => {
ref.current.scrollTop = 100;
}, []);
// Правильно: прокрутка відбувається до першого paint
useLayoutEffect(() => {
ref.current.scrollTop = 100;
}, []);Важкі обчислення в useLayoutEffect:
// Неправильно: блокує paint на 100мс+, інтерфейс зависає
useLayoutEffect(() => {
for (let i = 0; i < 1_000_000; i++) { /* важка робота */ }
}, []);Колбек useLayoutEffect має виконуватись менше 5мс. Все важче переноси в useEffect.
Забуття про SSR:
useLayoutEffect видає попередження в Node.js: Warning: useLayoutEffect does nothing on the server. Якщо компонент рендериться на сервері (Next.js, Remix), переходь на useEffect або перевіряй typeof window !== 'undefined'.
Де зустрічається в реальних проектах
- Framer Motion читає DOM-layout у
useLayoutEffectперед записом значень анімації - Next.js
<Image>використовуєuseLayoutEffectдля розміру placeholder - TanStack Query v5 використовує
useEffectдля запитів,useLayoutEffectдля синхронізації кешу таблиць - React DevTools використовує
useLayoutEffectдля вимірювань в інспекторі - Redux Toolkit використовує
useEffectдля підписок на store
Питання на співбесіді
Q: Чи може useLayoutEffect погіршити продуктивність?
A: Так, бо блокує paint. Якщо колбек займає більше кількох мілісекунд, браузерний фрейм затримується і інтерфейс підвисає. Chrome DevTools позначає це як long task.
Q: Що відбувається з useLayoutEffect при SSR?
A: React виводить попередження і хук нічого не робить на сервері. useEffect безпечний для SSR, бо виконується тільки в браузері.
Q: Який порядок спрацювання cleanup?
A: Обидва підтримують cleanup-функції. Cleanup у useLayoutEffect запускається синхронно до наступного layout-ефекту. У useEffect - асинхронно перед наступним ефектом.
Q: У React 18 з concurrent mode, useLayoutEffect все ще блокує?
A: Так, залишається синхронним і до paint навіть у concurrent mode. startTransition відкладає оновлення стану, але useLayoutEffect все одно спрацьовує до paint. Для несрочних побічних ефектів у transitions використовуй useEffect.
Приклади
Вимірювання висоти елемента перед paint
function TodoItem({ text, onDelete }) {
const ref = useRef();
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
// Вимірюємо до paint - без видимого стрибка макету
setHeight(ref.current.getBoundingClientRect().height);
});
return (
<div ref={ref} style={{ minHeight: `${height}px`, transition: 'height 0.2s' }}>
{text}
<button onClick={onDelete}>Видалити</button>
</div>
);
}getBoundingClientRect читає реальні розміри DOM. В useEffect браузер спочатку намалював би неправильну висоту - видно стрибок при додаванні або видаленні елемента. useLayoutEffect вимірює і виправляє до того, як юзер щось побачить.
Завантаження даних з useEffect
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data);
});
return () => { cancelled = true; };
}, [userId]);
if (!user) return <p>Завантаження...</p>;
return <p>{user.name}</p>;
}Завантаження даних не має відношення до pre-paint layout. useEffect тут правильний вибір. Прапорець cancelled запобігає оновленню стану розмонтованого компонента - поширене джерело попереджень про витік пам'яті в React DevTools.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.