Чому transform кращий для анімацій, ніж top, left
CSS transform переміщує елемент, застосовуючи матрицю до вже намальованого шару, тому браузер пропускає layout і paint на кожному кадрі анімації. top і left змінюють місце елемента в документі, що змушує браузер перераховувати все знову щокадру.
Теорія
TL;DR
top/left- як переставляти меблі: все навколо зсувається.transform- як посунути зображення проєктора: кімната стоїть на місці.- Браузер рендерить у три кроки: layout, paint, composite.
transformторкається лише останнього.top/leftзапускає все з початку. - Правило вибору: будь-який візуальний рух (hover, scroll-тригер, slide-in) -
transform.top/leftлишаються для статичного позиціювання, яке не анімується. - GPU обробляє
transformасинхронно, поза головним потоком.top/leftнавантажує CPU на кожні 16 мс кадру.
Короткий приклад
/* Погано: браузер перераховує layout щокадру */
@keyframes bad {
to { top: 50px; left: 50px; } /* layout + paint кожен кадр */
}
/* Добре: браузер пропускає layout і paint повністю */
@keyframes good {
to { transform: translate(50px, 50px); } /* тільки composite */
}
.box-bad { position: relative; animation: bad 1s infinite; }
.box-good { animation: good 1s infinite; }На складній сторінці bad просідає до 10-20fps під час скролу. good тримає 60fps, бо GPU обробляє анімацію незалежно від головного потоку.
Головна різниця
Браузер рендерить у три кроки: layout (розрахунок позицій), paint (малювання пікселів), composite (складання шарів). Анімація transform переносить елемент на окремий GPU-шар і оновлює лише composite-крок кожного кадру. top і left скасовують геометрію елемента, браузер іде вверх по дереву layout, перераховує позиції від кореня документа, потім ставить у чергу paint для всіх зачеплених зон. Один кадр коштує кілька мілісекунд, а при 60fps бюджет на кадр - 16 мс загалом.
Коли що використовувати
- Візуальний рух (hover-підняття, slide-in, паралакс):
transform: translate - Масштабування без зміщення сусідів:
transform: scale - Обертання або flip-анімація:
transform: rotate - Статичне позиціювання, точний контроль потоку:
top/leftпідходить - Mobile або критичний 60fps: завжди
transform
Таблиця порівняння
| Аспект | transform | top / left |
|---|---|---|
| Кроки рендерингу | Тільки composite (GPU) | Layout + Paint + Composite |
| Типовий fps під навантаженням | 60fps | 10-20fps |
| Впливає на потік документа | Ні | Так, зсуває сусідів |
| GPU-прискорення | Автоматичне виділення шару | Ні (CPU) |
| Продуктивність при скролі | Не змінюється | Може запускати reflow |
| Коли використовувати | Всі анімації, переходи | Тільки статичне позиціювання |
Як браузер обробляє це
Коли анімується transform, рушій рендерингу Chrome (Blink) переносить елемент на cc::Layer і передає його GPU-бекенду (Skia). Наступні кадри оновлюють лише composite-матрицю, без дерева layout. top/left скасовує геометрію RenderBox: браузер обходить дерево layout, позначає вузли як застарілі, перераховує позиції, потім ставить у чергу paint для всіх зачеплених ділянок. На сторінці з сайдбаром, хедером і floating-картками - це багато вузлів щокадру.
will-change: transform підказує браузеру виділити шар завчасно, уникаючи затримки на перший кадр. Але застосовувати його варто лише на елементах, які справді будуть анімуватися. will-change: transform одночасно на 100+ елементах може з'їсти 200 МБ GPU-пам'яті і покласти вкладку.
Типові помилки
Анімація width або height для ефекту росту:
/* Запускає перерахунок layout кожного кадру */
@keyframes grow-bad { to { width: 200px; } }
/* Правильно: scale не торкається layout */
@keyframes grow-good { to { transform: scale(2); } }position: relative + top для slide-in:
/* Зсуває всі наступні елементи під час анімації */
@keyframes slide-bad { from { top: -20px; } to { top: 0; } }
/* Не торкається потоку документа взагалі */
@keyframes slide-good {
from { transform: translateY(-20px); }
to { transform: translateY(0); }
}Надмірне використання will-change на статичних списках:
/* Витікає GPU-пам'ять на 50+ елементах */
.list-item { will-change: transform; }
/* Краще: виділяй шар тільки при hover */
.list-item:hover { will-change: transform; }Поєднання transform з margin для зміщення:
/* margin: 0 auto центрує в потоці; translate-зміщення конфліктує з ним */
.bad { margin: 0 auto; transform: translateX(10px); }
/* Правильно: ланцюг transform для комбінованого зміщення */
.good { left: 50%; transform: translateX(-50%) translateX(10px); }Де зустрічається
- React Transition Group використовує
transform: translate3dдля анімацій переходів між сторінками - Framer Motion за замовчуванням рухає
motion.divчерезtransform - GSAP компілює
x: 100уtranslateX(100px)всередині - Swiper.js запускає свайпи каруселі через
translate3d - Tailwind
animate-pulseвикористовуєscaleіopacity, а неwidth/height
Питання на співбесіді
Q: Чому transform не впливає на layout, навіть якщо елемент рухається візуально?
A: transform застосовує матрицю до намальованого шару після того, як layout завершено. Потік документа бачить оригінальний нетрансформований блок. Сусідні елементи не знають, що щось змінилось візуально.
Q: Коли браузер виділяє елемент в окремий composite-шар?
A: При анімації transform, opacity або 3D-властивостей, а також при will-change: transform. Перевірити поточний стан шарів можна у вкладці Layers у Chrome DevTools.
Q: transform дає таку ж піксельну точність, як top/left?
A: Так. Дробові значення типу translateX(0.5px) рендеряться через GPU subpixel blending, що навіть плавніше. Якщо бачиш розмиті краї - додай backface-visibility: hidden для чистого шару.
Q: Як відлагодити зависання анімацій у продакшені?
A: Відкрий вкладку Performance у DevTools, запиши під час анімації. Шукай "Recalculate Style" і "Layout" у flame chart. Ці піки вказують на layout thrash. Чиста transform-анімація показує лише "Composite Layers".
Q: Є особливості у Safari?
A: На iOS 12 і раніше потрібен був transform: translateZ(0) для примусового виділення шару. З iOS 13 звичайний transform працює без хаку. Все одно краще перевіряти на реальних пристроях, а не тільки в симуляторі.
Приклади
Підняття картки при hover
Поширений патерн у продуктових інтерфейсах: картка злегка піднімається при наведенні. Неправильний варіант - top: -8px при hover. Це зсуває картку в потоці, сторінка «стрибає», запускається layout. Правильний варіант:
.card {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.card:hover {
transform: translateY(-8px) scale(1.02);
}Сусіди не зсуваються. Перерахунку layout немає. GPU обробляє весь перехід, і ефект однаково плавно працює як на одній картці, так і в гриді з сотнею.
Slide-in нотифікація
// React-компонент: нотифікація виїжджає справа
function Notification({ visible, message }) {
return (
<div
style={{
transform: visible ? 'translateX(0)' : 'translateX(110%)',
transition: 'transform 0.3s ease-out',
position: 'fixed',
right: 16,
bottom: 16,
}}
>
{message}
</div>
);
}position: fixed визначає де елемент знаходиться у viewport. transform відповідає за анімацію. Це два різних обов'язки, і саме їх розділення робить патерн плавним. Анімувати right замість transform - помилка, яку я бачив у кількох продакшн-кодбазах. Локально виглядає нормально, а на бюджетному Android дає помітний джанк.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.