Skip to main content

Чому 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 мс кадру.

Короткий приклад

css
/* Погано: браузер перераховує 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

Таблиця порівняння

Аспектtransformtop / left
Кроки рендерингуТільки composite (GPU)Layout + Paint + Composite
Типовий fps під навантаженням60fps10-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 для ефекту росту:

css
/* Запускає перерахунок layout кожного кадру */ @keyframes grow-bad { to { width: 200px; } } /* Правильно: scale не торкається layout */ @keyframes grow-good { to { transform: scale(2); } }

position: relative + top для slide-in:

css
/* Зсуває всі наступні елементи під час анімації */ @keyframes slide-bad { from { top: -20px; } to { top: 0; } } /* Не торкається потоку документа взагалі */ @keyframes slide-good { from { transform: translateY(-20px); } to { transform: translateY(0); } }

Надмірне використання will-change на статичних списках:

css
/* Витікає GPU-пам'ять на 50+ елементах */ .list-item { will-change: transform; } /* Краще: виділяй шар тільки при hover */ .list-item:hover { will-change: transform; }

Поєднання transform з margin для зміщення:

css
/* 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. Правильний варіант:

css
.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 нотифікація

jsx
// 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 дає помітний джанк.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?