Усе працює, доки ви не відправите в продакшн «тільки невелику анімацію», і ваша сторінка не почне рухатися ніби крізь патоку. Найгірше: вона може виглядати плавно на вашому ноутбуці, а перетворитися на слайдшоу на середньому телефоні, яким справді користуються ваші клієнти.
Продуктивні CSS-анімації — це не містика. Це проблема конвеєра рендерингу. Якщо ви розумієте, що викликає layout, paint і compositing — і перевіряєте це правильними інструментами — ви можете випустити рух, який відчувається «дорого», без реальних витрат.
Конвеєр рендерингу, за який ви фактично платите
Браузери не «анімують CSS». Вони оновлюють граф сцени під жорсткими дедлайнами: ~16,7ms за кадр для 60Hz, ~8,3ms для 120Hz. Пропустите дедлайн — і користувач побачить підлагування. І користувачі безжальні: вони звинуватять ваш продукт, а не свій пристрій.
Для роботи з продуктивністю зведіть усе до трьох витрат:
- Layout (перерахунок розмітки, reflow): обчислення розмірів і позицій елементів. Дорого тому, що зміни можуть каскадувати.
- Paint: растеризація пікселів (текст, обводки, тіні, зображення) у бітмапи. Дорого, бо пікселі — це робота.
- Composite: збір уже намальованих шарів у фінальний кадр, застосовуючи трансформації, прозорість, обтинання тощо. Часто дешевше і може виконуватися на композиційному потоці.
Коли люди кажуть «використовуйте transform та opacity», вони мають на увазі практичну істину: ці властивості часто можна анімувати на етапі композитингу без повторного виконання layout або paint кожного кадру.
Що дає «тільки композитор» насправді
Якщо браузер може тримати елемент на власному шарі (або інакше обробляти його як окрему поверхню), зміна transform або opacity стає множенням матриці та alpha-blend’ом. Це не безкоштовно, але передбачувано. А передбачуваність — саме те, що зберігає ваш бюджет кадру, коли інша частина сторінки зайнята… всім іншим.
Але «тільки композитор» — це умовна обіцянка, а не закон фізики. Ви все ще можете викликати paint (а іноді й layout), якщо елемент не ізольований, якщо він перетинається з ефектами, що потребують перерисовки, або якщо ви просите браузер зробити те, що не можна відкласти до композитингу.
Рамки SRE: продуктивність як SLO
В операціях продакшн ми не приймаємо «зазвичай нормально» для затримок. Продуктивність руху заслуговує тієї ж дисципліни. Ставтеся до джанку як до хвостової латентності: пара поганих кадрів у 99-му процентилі може визначити, як сприймається інтерфейс. Вам потрібні:
- усвідомлення бюджету (16,7ms — ваш таймаут запиту)
- профілювання (ваші трасування)
- детекція регресій (ваші алерти)
- запобіжники (ваші ліміти частоти)
І так, ви цілком можете погіршити продуктивність анімацій, не торкаючись самої анімації. Додали тінь. Змінили шрифт. Запустили новий фіксований хедер. Вітаємо, ваша композитна анімація тепер чекає за роботою paint.
Правило transform/opacity (і що воно насправді означає)
Правило просте: анімуйте transform і opacity, коли вам важлива плавність. Причина менш проста: ці властивості можуть застосовуватися під час композитингу до попередньо намальованих текстур, уникаючи щокадрових layout і paint.
Хороші анімації: змінюють спосіб відтворення, а не саму структуру
Використовуйте transform для переміщення і масштабування, а opacity для появи/зникання. Замість анімації top або left анімуйте transform: translate(). Замість анімації width анімуйте transform: scaleX() на псевдоелементі або внутрішній обгортці.
Погані анімації: змінюють геометрію, потік чи важкі для paint ефекти
Уникайте анімації властивостей, що змушують layout:
width,heighttop,left,right,bottom(коли вони впливають на layout)margin,paddingfont-size,line-height(особливо болісно для тексту)
Уникайте анімації властивостей, що викликають дорогі paints:
box-shadow(великий blur — це податок на paint)filter(іноді компонується, іноді жорстоко; залежить від браузера й контексту)background-position(може бути важким для paint)border-radius(часто тригерить перерисовку; може бути несподівано дорогим у масштабі)
«Але мені потрібно анімувати висоту» — дорослі альтернативи
Іноді треба розгортати панель. У вас все ще є варіанти, що не підпалять CPU:
- Використайте трансформи на внутрішній обгортці: зберігайте стабільність layout і анімуйте обрізаний внутрішній елемент з
transform: scaleY(). Додайтеtransform-origin: top. - Використайте
max-heightлише для невеликих діапазонів: це все ще тригерить layout, але шкода може бути обмежена, якщо піддерево ізольоване і невелике. - Використайте дискретні стани + зменшений рух: іноді правильний фікс — менше кадрів.
- Використайте Web Animations API для оркестрації, але зберігайте ті самі вибори властивостей. API не робить layout дешевшим сам по собі.
Обирайте правильний easing і тривалість (так, це важливо)
Занадто короткі анімації виглядають як глітчі; занадто довгі — як затримка інтерфейсу. Хороший дефолт: 150–250ms для мікро-інтеракцій і 250–400ms для більших переходів. Якщо анімуєте позицію на велику відстань, додайте трохи часу, інакше це буде нагадувати телепортацію.
Також: не складайте три easing’и, що борються між собою. Якщо елемент масштабуються, рухається і змінює прозорість, тримайте криву послідовною, якщо немає вагомої причини інакше.
Жарт #1: Анімувати height у великому DOM — це як «просто перезапустити базу даних» під час пікового трафіку: іноді спрацьовує, і вам не варто радіти цьому щастю.
Цитата, бо це правда
Парафраз ідеї (Werner Vogels): «Усе ламається, весь час». Плануйте на це — особливо регресії продуктивності.
Факти та історичний контекст, які варто знати
Це не тривія заради тривії. Кожен пункт пояснює, чому порада «transform/opacity» існує і чому іноді вона провалюється.
- Ранні CSS-анімації відмальовувалися як будь-яка інша зміна стилю. Перехід до анімацій, керованих композитором, прискорився з появою багатопотокового рендерингу і агресивнішої політики створення шарів у браузерах.
- Мобільні браузери підштовхнули розвиток. Десктопні CPU могли «навалити» багато поганих анімацій. Мобільні теплові обмеження зробили джанк неминучим, якщо робота не переміститься з головного потоку.
- Дисплеї з високою частотою оновлення змінили планку. 120Hz робить посередню анімацію помітно гіршою, бо часу на кадр удвічі менше.
- «GPU-прискорення» — не єдиний перемикач. Композитинг може використовувати GPU; растеризація усе ще може бути на CPU; і деякі ефекти примушують програмні fallback’и залежно від драйверів та тиску пам’яті.
- Створення шарів має свою вартість. Просування надто багатьох елементів до шарів може підвищити використання пам’яті і навантаження на композитинг. «Фікс» стає новою проблемою.
- Рендеринг шрифтів — частий прихований податок. Анімації, що змушують текст перерисовуватися — особливо з змінами субпіксельного згладжування — можуть різко погіршити продуктивність і виглядати нестабільно візуально.
- Sticky і fixed елементи ускладнюють pipeline скролу. Багато браузерів оптимізують скрол, утримуючи його поза головним потоком; певні ефекти (наприклад важкі backdrops) можуть повернути його назад.
- Існують примітиви containment, бо layout заразливий. CSS
containіcontent-visibilityз’явилися, щоб зменшити радіус ураження роботи layout/paint на складних сторінках. prefers-reduced-motionз’явився не просто так. Це не тільки доступність; це також «вихідна гілка» для пристроїв з повільною продуктивністю.
Підводні камені: коли «GPU-прискорено» стає «GPU-набридло»
1) Transform/opacity — швидкі… поки ви все одно не примусите paint
Ви можете анімувати transform на елементі, який усе одно перерисовується, оскільки:
- елемент не на власному шарі і браузер вирішує, що перерисовка дешевша за ізоляцію
- всередині є нащадки з важкими для paint ефектами, які змінюються (наприклад анімовані градієнти)
- ви поєднуєте це з ефектами, що вимагають перерисовки (деякі фільтри, режими змішування, великі тіні)
Правило: якщо пікселі всередині елемента стабільні — композитинг перемагає. Якщо пікселі змінюються — ви платите за paint і transform вам не допоможе.
2) will-change — це інструмент підвищеної потужності, а не спосіб життя
will-change: transform каже браузеру: «я збираюся анімувати це; підготуйтеся». Підготовка часто означає просування в шар і виділення пам’яті. Це корисно для невеликої кількості елементів, які ви знаєте, що анімуються незабаром.
Шкідливо, коли ви розсипаєте його всюди «на випадок». Режими відмов при цьому:
- зростання використання GPU-пам’яті (більше шарів, великі текстури)
- більше роботи з композитингом (більше поверхонь для блендінгу)
- більший тиск на кеш (текстури викидаються та перерисовуються пізніше)
- гірша продуктивність на слабких пристроях (саме там, де потрібна допомога)
Користуйтеся will-change як попереднім прогрівом кеша: близько до події, строго по області, видаліть коли не потрібен.
3) Субпіксельне тремтіння і скарги «чому розмите?»
Трансформації відбуваються у плаваючому просторі. Текст і тонкі лінії можуть потрапити на половинні пікселі, викликаючи відмінності в антиаліасингу кадр-до-кадру. Ви побачите це як мерехтіння або розмиття під час руху.
Виправлення включають:
- транслювати по цілих пікселях коли можливо (округляйте значення в JS якщо керуєте трансформом)
- уникати прямої анімації шарів з текстом; анімуйте контейнери зі стабільною растеризацією
- обережно розглядати
translateZ(0)(воно може змінити поведінку растеризації)
4) Великі шари — дорогі шари
Якщо ви просуваєте елемент на весь екран (або близько до нього) на власний шар і анімуєте його, ви можете виділити величезні текстури. Це може:
- збільшити використання пам’яті
- спровокувати тайлінг та часткові перерисовки
- спровокувати викид пам’яті GPU, що викликає підлагування в найгірший момент
Один з найпоширеніших «чому стало гірше?» моментів — просування великого скролюваного контейнера, бо це здавалося гарною ідеєю.
5) Анімації, що конфліктують зі скролом
Скрол — святе. Браузери наполегливо працюють, щоб скрол був плавним, іноді виконуючи його поза головним потоком. Якщо ваша анімація змушує роботу головного потоку під час скролу — layout, важкий paint або синхронний JS — скрол-джанк з’являється миттєво.
Будьте особливо обережні з:
- JS, що керує скролом і читає layout та записує стилі в одному тіку
- липкі хедери з важкими тінями/backdrops
- великими зонами
backdrop-filter(часто дорогі)
Жарт #2: Вашому обробнику скролу не потрібно бути «реального часу». Це UI, а не високочастотний трейдинг.
Швидкий план діагностики
Це чекліст «сторінка підлаговує, що робити в наступні 10 хвилин?». Він пріоритезує найпоширеніші вузькі місця і найшвидші кроки для розрізнення причин.
Спершу: підтвердіть, який це джанк
- Чи під час скролу? Якщо так — підозрюйте роботу головного потоку (layout/paint/JS), що блокує скрол, або дорогий композитинг.
- Чи під час конкретної анімації? Якщо так — підозрюйте layout-thrash, paint-важкі ефекти, забагато шарів або великі текстури.
- Чи лише на деяких пристроях? Якщо так — підозрюйте обмеження пам’яті GPU, різниці драйверів, високий DPR або теплове тротлінг.
По-друге: заміряйте перед «оптимізацією»
- Запишіть трасу в DevTools Performance з працюючою анімацією.
- Перевірте, чи кадри пропадають через Main (JS/layout) або Raster/Paint чи Compositor.
- Увімкніть підсвічування перерисовок / меж шарів, щоб бачити, що перерисовується і що компонується.
По-третє: застосуйте мінімальний фікс, який усуває вузьке місце
- Якщо домінує layout: припиніть анімацію властивостей layout; додайте containment; приберіть примусове синхронне читання layout у JS.
- Якщо домінує paint: зменшіть область перерисовки; приберіть дорогі тіні/фільтри; ізолюйте анімований елемент; попередньо відрендерте статичні активи.
- Якщо домінує композитинг: зменшіть кількість шарів; уникайте величезних просунутих поверхонь; приберіть непотрібний
will-change.
По-четверте: валідуйте на реалістичному профілі пристрою
Тротльте CPU у DevTools, тестуйте на високому DPR і зменшеному русі. Якщо фікс працює лише на вашій машині — це не фікс; це демо.
Практичні завдання: команди, виводи, рішення
Ви просили практичні завдання з командами, виводами та рішеннями. Ось 12+, що відповідають реальним робочим процесам: локальна налагодка, CI-перевірки та розслідування «чому це ламається тільки в проді?».
Завдання 1: Проведіть аудит, які CSS-властивості анімуються в кодовій базі
cr0x@server:~$ rg -n "transition-property|transition:|@keyframes|animation:" ./src
src/components/Card.css:14:transition: box-shadow 200ms ease, transform 200ms ease;
src/components/Drawer.css:22:transition: height 250ms ease;
src/styles/animations.css:5:@keyframes pulse { from { opacity: 0.6 } to { opacity: 1 } }
Що це означає: У вас є принаймні одна транзіція height і анімація box-shadow — класичні ризики для layout/paint.
Рішення: Замініть анімацію height технікою на основі трансформів або ізолюйте її до невеликого піддерева. Розгляньте відмову від анімації тіні або її імітацію через прозорість на попередньо відрендереному елементі тіні.
Завдання 2: Знайдіть використання «will-change» і перевірте, чи його не зловживають
cr0x@server:~$ rg -n "will-change" ./src
src/styles/global.css:88:.card { will-change: transform; }
src/components/Modal.css:12:.modal { will-change: transform, opacity; }
src/components/List.css:3:.row { will-change: transform; }
Що це означає: Багато елементів попередньо просочені, можливо включно з рядками списку (потенційно сотні).
Рішення: Приберіть will-change з будь-чого, що повторюється в великих списках. Застосовуйте його лише безпосередньо перед взаємодією (hover/focus/open) і видаляйте після.
Завдання 3: Перевірте, чи ви підтримуєте reduced-motion
cr0x@server:~$ rg -n "prefers-reduced-motion" ./src
src/styles/animations.css:42:@media (prefers-reduced-motion: reduce) { .anim { animation: none !important; transition: none !important; } }
Що це означає: Є як мінімум базова вимикачка.
Рішення: Переконайтеся, що вона покриває важкі інтеракції (шухляди, каруселі) і не просто вимикає декоративні анімації, лишаючи дорогі layout-переходи.
Завдання 4: Зловіть патерни layout-thrashing у JS (чередування читання/запису)
cr0x@server:~$ rg -n "offsetHeight|getBoundingClientRect\\(|scrollTop|clientWidth" ./src
src/pages/Home.ts:91:const h = panel.offsetHeight;
src/pages/Home.ts:92:panel.style.height = (h + 20) + "px";
src/hooks/useSticky.ts:44:const r = el.getBoundingClientRect();
Що це означає: Є принаймні один кандидат читання-після-запису, який може змусити синхронний layout кожного кадру, якщо використовується у циклі або в обробнику скролу.
Рішення: Групуйте читання і запис (спочатку зберіть усі виміри, потім виконайте зміни), або перейдіть на підхід на основі трансформів, що уникає читання layout у «гарячому» шляху.
Завдання 5: Зробіть Lighthouse JSON-артефакт у CI (базовий бюджет продуктивності)
cr0x@server:~$ lighthouse http://localhost:4173 --output=json --output-path=./artifacts/lh.json --quiet
...Auditing: Performance...
...Report is done...
...Saved JSON report to ./artifacts/lh.json...
Що це означає: У вас є відтворюваний артефакт для дифу між комітами. Lighthouse не «оцінив» вашу анімацію, але він вловить завантаження головного потоку і великі витрати на paint, що корелюють із джанком.
Рішення: Додайте пороги (наприклад total blocking time, main-thread work) як запобіжники; коли вони регресують — плавність анімацій теж, скоріш за все, регресує.
Завдання 6: Витягніть довгі таски з Lighthouse-артефакту (виявлення виснаження головного потоку)
cr0x@server:~$ jq '.audits["long-tasks"].details.items[:5]' ./artifacts/lh.json
[
{
"startTime": 1234.56,
"duration": 245.12,
"url": "http://localhost:4173/assets/app.js",
"attributableToMainThread": true
}
]
Що це означає: Довгі таски понад ~50ms вбивають кадри; вони блокують ввід і анімації.
Рішення: Розділяйте роботу (code splitting), відкладіть неключовий JS, уникайте важких обчислень під час переходів/скролу.
Завдання 7: Захопіть CPU-профіль під час відтворення джанку (Node-інструменти для dev-серверів)
cr0x@server:~$ node --cpu-prof --cpu-prof-dir=./profiles ./node_modules/.bin/vite dev
VITE v5.0.0 ready in 312 ms
➜ Local: http://localhost:5173/
...CPU profile written to ./profiles/CPU.2025-12-29T10-22-11.123Z.cpuprofile...
Що це означає: Якщо ваш dev-сервер вантажить CPU (гарячі перезавантаження, тяжкі трансформації на кроці збірки), ви можете плутати інструментальний джанк з ап-джанком.
Рішення: Якщо dev-сервер — вузьке місце, тестуйте в production-збірці. Не оптимізуйте CSS на основі артефактів у dev-режимі.
Завдання 8: Зберіть production-збірку і подайте її локально (прибрати шум dev-режиму)
cr0x@server:~$ npm run build
> build
...dist/assets/index-abc123.js 312.45 kB...
cr0x@server:~$ npx serve -s dist -l 4173
Serving!
Local: http://localhost:4173
Що це означає: Ви тепер тестуєте те, що отримують користувачі: мінімізований JS, оптимізований CSS, реальна поведінка бандлінгу.
Рішення: Знову перевірте джанк анімацій у production-режимі перед змінами. Якщо проблема зникає — винен інструмент або source maps, а не CSS.
Завдання 9: Використайте Playwright, щоб отримати детерміністичну трасу під час анімації
cr0x@server:~$ npx playwright test --trace on --project chromium
Running 1 test using 1 worker
✓ ui-animations.spec.ts:12:1 drawer open should be smooth (4.2s)
Trace file: test-results/ui-animations-drawer/trace.zip
Що це означає: Ви можете відтворити те, що сталося, і зіставити з активністю JS та таймінгом. Це найприближеніше до «perf regression tests», які не зруйнують вам життя.
Рішення: Якщо коміт значно змінює трасування (більше довгих тасків під час анімації), блокуйте merge або виправляйте регресію до релізу.
Завдання 10: Перевірте наявність гігантських зображень, що перетворюють фейд у pipeline декодування→пейїнту
cr0x@server:~$ find ./dist -type f -name "*.png" -o -name "*.jpg" -o -name "*.webp" | xargs -I{} sh -c 'printf "%8s %s\n" "$(stat -c%s "{}")" "{}"' | sort -nr | head
5242880 ./dist/assets/hero-background.jpg
1310720 ./dist/assets/product-shot.png
786432 ./dist/assets/logo.png
Що це означає: Великі активи збільшують витрати на декод і растеризацію. Фейд 5MB hero background все ще може підлагувати, якщо декод/растеризація спрацює під час переходу.
Рішення: Зміншіть/скомпресуйте активи; попередньо завантажуйте критичні зображення; уникайте анімації величезних щойно декодованих поверхонь у видимість.
Завдання 11: Перевірте проксі-рахунки шарів, знаходячи масово застосовані трансформи
cr0x@server:~$ rg -n "transform:" ./src | head -n 10
src/components/Row.css:7:transform: translateZ(0);
src/components/Card.css:22:transform: translateY(-2px);
src/components/Toast.css:18:transform: translateX(0);
src/components/Toast.css:22:transform: translateX(120%);
Що це означає: translateZ(0) часто використовується як «хак, щоб примусити шар». Якщо використовується широко, воно може роздмухати кількість шарів і пам’ять.
Рішення: Видаліть загальний translateZ(0). Додавайте просування в шар лише там, де профілювання показує виграш.
Завдання 12: Знайдіть дорогі CSS-ефекти, що часто перерисовуються (тіні, фільтри, backdrops)
cr0x@server:~$ rg -n "box-shadow:|filter:|backdrop-filter:" ./src
src/styles/global.css:55:box-shadow: 0 20px 60px rgba(0,0,0,0.35);
src/components/Header.css:19:backdrop-filter: blur(12px);
src/components/Avatar.css:9:filter: drop-shadow(0 6px 10px rgba(0,0,0,0.25));
Що це означає: Це часті винуватці paint’а, особливо у поєднанні з анімацією або скролом.
Рішення: Зменшіть радіус blur, скоротіть область впливу або замініть на попередньо відрендерені ресурси. Для backdrops: звужте зону blur або надайте fallback без розмиття для слабких пристроїв.
Завдання 13: Швидко виявити, чи ви анімуєте layout через shorthand-переходи
cr0x@server:~$ rg -n "transition:\s*all" ./src
src/components/Button.css:4:transition: all 200ms ease;
src/components/Panel.css:11:transition: all 300ms ease-in-out;
Що це означає: transition: all — це «пастка». Воно може почати анімацію властивостей, що впливають на layout, випадково, коли хтось пізніше змінить CSS.
Рішення: Замініть на явні властивості (наприклад, transition: transform 200ms ease, opacity 200ms ease) і зафіксуйте це через linting.
Завдання 14: Додайте швидке правило stylelint, щоб завадити регресіям «transition: all»
cr0x@server:~$ cat .stylelintrc.json
{
"rules": {
"declaration-property-value-disallowed-list": {
"transition": ["/all\\s/"]
}
}
}
Що це означає: Це блокує найпоширенішу випадкову регресію продуктивності в CSS-переходах.
Рішення: Включіть це в CI, робіть збірку невдалою при порушеннях і змушуйте використовувати явні переходи, щоб характеристики продуктивності були стабільними з часом.
Три корпоративні міні-історії з передової
Міні-історія 1: Інцидент через хибне припущення
Продуктова команда випустила редизайн дашборду з ефектною анімацією «карти плавають». Реалізація була дисциплінованою: тільки transform і opacity. Усі потиснули один одному руку, бо запам’ятали правило.
Через кілька днів служба підтримки почала отримувати тікети: «скрол зависає», «кнопки не реагують», «сторінка лагає». На флагманських пристроях це не відтворювалося. Відтворювалося на старих телефонах і деяких корпоративних ноутбуках з консервативними GPU-драйверами.
Хибне припущення було простим: transform/opacity завжди означає тільки композитинг. У цьому випадку кожна карта містила тонкий анімований градієнтний шиммер (ефект «скелетон-лдінгу») як анімацію фону. Ці фони перерисовувалися. Отже «дешевий» transform на 30 картках став «перерисувати 30 карток плюс композитинг» кожного кадру.
Фікс не був героїчним. Видалили шиммер після завантаження даних, зменшили кількість одночасно анімованих карток і обмежили перерисовку обгорткою з containment. Також додали fallback для prefers-reduced-motion, який повністю вимикав шиммер. Дашборд перестав підлаговувати, і команда тихо прибрала «GPU-accelerated» зі своєї внутрішньої презентації.
Міні-історія 2: Оптимізація, що відбилася бумерангом
Інша організація мала систему модалів, що використовувалася по всьому додатку. Хтось помітив, що відкриття модалу іноді пропускає кадри, тож вони «оптимізували» це, додавши will-change: transform, opacity до модалу і оверлею, плюс translateZ(0) до кількох компонентів «щоб бути впевненими, що воно залишається на GPU».
В ізольованому випадку це спрацювало. Але внесло повільну деградацію продуктивності в інших місцях. Сторінки з довгими списками стали помітно гіршими після кількох взаємодій. Пам’ять процесу GPU росла і ніяк не спадала швидко. Користувачі описували це як «він стає гіршим, чим довше я ним користуюся», що дуже точно відображає симптом.
Коріння проблеми: повсюдне просування шарів. Рядки списку з will-change стали власними поверхнями. Оверлей і модал залишалися просунутими навіть коли не анімувався. Композитор мав більше роботи і більше тиск на пам’ять. Під навантаженням текстури викидалися, потім перерисовувалися пізніше — що викликало джанк у зовсім інших взаємодіях.
Відкат був простим: прибрали хаке translateZ(0), обмежили will-change лише коротким вікном перед анімацією і явно очищали його після завершення анімації. Продуктивність повернулась, і скарги про повільне деградування зникли. Урок: will-change — не спеція, яку сиплють усюди.
Міні-історія 3: Нудна, але правильна практика, що врятувала ситуацію
Команда платформи підтримувала дизайн-систему, що використовувалася десятками продуктових команд. Їх вже обпікали регресії анімацій, тож вони зробили щось глибоко непоказне: кодифікували примітиви анімацій і заборонили ризикові переходи за замовчуванням.
Кнопки, картки, тости й шухляди використовували спільні міксини: трансформи для руху, opacity для фейдів. Ні transition: all. Ні анімації властивостей layout у загальних компонентах. Якщо компоненту справді потрібна анімація висоти — це має бути ізольовано через патерн обгортки і задокументовано.
Потім прийшов великий ребренд: нова типографія, важчі тіні, більше розмиття. Додаток мав стати джанк-ошарпаним. Натомість шкода була локалізована, бо базові анімації не залежали від paint-важких властивостей. Коли деякі екрани регресували, трасування було читабельним: ви могли вказати на спайки по часу paint від нових візуальних ефектів, замість ганятися за фантомними «багами CSS-анімації».
Вони встигають у терміни, а черга інцидентів залишилась тихою. Ніхто не писав святкового поста про це, бо єдина помітна зміна була в тому, що нічого не загорілося. Ось це і є робота.
Типові помилки: симптом → корінь → виправлення
1) Симптом: анімація підлаговує лише на старті
Корінь: просування в шари і растеризація відбуваються пізно (перший кадр анімації), або одночасно декодується зображення/шрифт.
Виправлення: попередньо промотуйте коротко з will-change безпосередньо перед анімацією; preload критичних зображень; уникайте заміни шрифтів під час анімації.
2) Симптом: hover-ефекти відчуваються «важкими» і лагають ввід
Корінь: hover тригерить paint-важкі властивості (blur тінь) на багатьох елементах в гріді; область перерисовки велика.
Виправлення: анімуйте transform/opacity; імітуйте тіні через opacity на псевдо-елементі; зменшіть blur; скоротіть кількість одночасно hover-ованих елементів.
3) Симптом: панелі, що розгортаються/згортаються, різко втрачають кадри
Корінь: анімація height/max-height викликає перерахунок layout великого піддерева кожного кадру.
Виправлення: збережіть layout стабільним і анімуйте внутрішню обгортку з transform: scaleY() з обрізанням; додайте contain: layout paint, де безпечно; уникайте читання layout кожен тик.
4) Симптом: скрол-джанк після додавання липкого хедера
Корінь: липкий елемент з дорогим paint (тінь/blur backdrop) примушує роботу головного потоку під час скролу; скрол не може лишатися повністю асинхронним.
Виправлення: спростіть візуал липкого елемента; зменшіть область blur/backdrop; розгляньте однотонний хедер для слабких пристроїв; перевірте paint flashing.
5) Симптом: текст виглядає розмитим під час анімації
Корінь: субпіксельне позиціонування під час трансформів; растеризація змінюється в міру руху елемента.
Виправлення: анімуйте контейнер, а не текст; округлюйте значення translate; уникайте масштабування тексту; тестуйте в різних браузерах (поведінка растеризації відрізняється).
6) Симптом: продуктивність погіршується з часом, а не одразу
Корінь: забагато просунутих шарів (часто через will-change або translateZ(0)) викликають тиск на пам’ять і churn текстур.
Виправлення: приберіть постійні просування; тримайте кількість шарів низькою; Scope-уйте will-change на активні анімації лише.
7) Симптом: «анімація transform повільна» на великому елементі
Корінь: елемент величезний; композитинг його кожного кадру дорогий; можуть відбуватися завантаження текстур або тайлінг.
Виправлення: зменшіть розмір шару (анімуйте менший дочірній елемент); уникайте просування поверхонь на весь екран; спростіть візуал; віддавайте перевагу лише прозорості для великих зон.
8) Симптом: анімація ламається, коли контент змінюється під час переходу
Корінь: змішування layout-змін з анімацією трансформу; підвантаження контенту тригерить reflow посеред анімації.
Виправлення: зафіксуйте розміри під час анімації; уникайте вставки DOM-вузлів «в польоті»; анімуйте заповнювачі, а потім міняйте контент після завершення.
Контрольні списки / покроковий план
Чекліст A: проєктування анімації, яка залишиться швидкою
- Визначте завдання: декоративна це чи функціональна анімація? Якщо функціональна — віддавайте пріоритет відповідальності над прикрасою.
- Обирайте властивості: за замовчуванням —
transform+opacity. Уникайте властивостей layout, якщо анімоване піддерево не крихітне. - Тримайте paint стабільним: уникайте анімації blur-важких тіней, фільтрів і великих градієнтів.
- Обмежуйте сферу впливу: анімуйте один контейнер, а не десятки дітей, якщо ви не профілювали це.
- Виберіть тривалості: 150–250ms для маленьких взаємодій; 250–400ms для більших переходів. Коротше — не завжди швидше.
- Плануйте reduced motion: вимикайте або спрощуйте ефект через
prefers-reduced-motion.
Чекліст B: відправка без регресій
- Забороніть
transition: allу спільних компонентах. Явні переходи зберігають продуктивність стабільною. - Лінтуйте ризикові патерни: масове
will-change, blankettranslateZ(0), читання layout у обробниках скролу. - Профілюйте на «повільному» профілі: тротльте CPU і тестуйте на середньому обладнанні.
- Записуйте трасування до/після: тримайте їх у PR, щоб уникнути «працює на моїй машині» дебатів.
- Міряйте довгі таски: продуктивність анімацій часто маскує проблему з плануванням JS.
Чекліст C: виправлення існуючої джанкої анімації
- Виявіть дорогий етап: головний потік vs paint vs композитинг.
- Якщо головний потік гарячий: усуньте анімації layout, групуйте DOM-читання/записи, скоротіть довгі таски.
- Якщо paint гарячий: спростіть візуал, зменшіть область перерисовки, приберіть blur-важкі ефекти, ізолюйте через containment.
- Якщо композитинг гарячий: зменшіть кількість шарів і їхній розмір, приберіть непотрібні просування.
- Провалідуйте знову: те саме відтворення, той самий профіль пристрою, той самий підхід до вимірювання.
FAQ
1) Чи завжди transform і opacity «безкоштовні» для анімації?
Ні. Вони часто дешевші, бо можуть застосовуватися на етапі композитингу, але ви все одно платите за композитинг, і контекст може змусити тригерити paint/layout.
2) Чи слід використовувати will-change скрізь, щоб примусити плавні анімації?
Ні. Використовуйте його економно і тимчасово. Надмірне використання підвищує тиск на пам’ять і навантаження на композитинг, що може погіршити продуктивність — особливо на слабких пристроях.
3) Чи все ще translateZ(0) хороший трюк?
Як таргетований обхід: іноді. Як дефолт: ні. Це грубий інструмент, який може створити надто багато шарів і спричинити довгострокову деградацію продуктивності.
4) Чому анімація box-shadow така важка?
Тому що великі розмиті тіні дуже важкі для paint. Якщо ви їх анімуєте, часто перерисовуєте велику область кожного кадру. Імітуйте їх через opacity на псевдоелементі, зменшіть blur або уникайте анімації тіні.
5) Що з анімацією filter або backdrop-filter?
Іноді вона компонується, іноді дорога, і вартість залежить від браузера та пристрою. Обробляйте фільтри як «спочатку профілюйте». Для backdrops суворо зменшуйте область впливу.
6) Якщо я анімую тільки трансформи, чому я все одно бачу джанк?
Поширені причини: довгі таски JS на головному потоці (блокують здатність композитора віддавати кадри), пізнє просування в шар, промахи кеша растерів, величезні шари або інші частини сторінки, що перерисовуються під час анімації.
7) Чи завжди CSS-анімація краща за JS-анімацію?
Ні. CSS добре підходить для простого декларативного руху. JS (або Web Animations API) краще підходить для оркестрації, переривань і секвенування. Але вибір властивостей важливіший за вибір API.
8) Як анімувати висувну шухляду без анімації height?
Використайте внутрішню обгортку, яку ви масштабуватимете по Y з transform: scaleY() і обрізанням overflow. Зовнішній layout має лишатися стабільним, щоб решта сторінки не перераховувалася кожного кадру.
9) Чому це виглядає плавно на десктопі, але не на мобільному?
Мобільні пристрої мають менші CPU/GPU бюджети, вищі DPR і теплове тротлінг. Ваша «мала» область paint може стати величезною в реальних пікселях, і тиск на пам’ять приходить швидше.
10) Чи варто вимкнути анімації для всіх, якщо продуктивність погана?
Не карайте всіх користувачів через підмножину пристроїв. Надайте prefers-reduced-motion, спростіть найважчі ефекти і виправляйте корінь проблеми. Вимикайте дійсно декоративні речі, якщо вони не виправдовують себе.
Висновок: наступні кроки, які ви насправді можете виконати
Якщо ви хочете плавні CSS-анімації в продакшні, припиніть ставитися до «transform і opacity» як до забобону і почніть ставитися до цього як до гіпотези, яку треба перевірити.
- Аудит: приберіть
transition: all, знайдіть властивості, що анімують layout, і видаліть загальні шарові хакі. - Профілювання: запишіть трасування і визначте, чи вузьке місце — layout, paint чи композитинг.
- Мінімальне виправлення: переключіться на transform/opacity, ізолюйте layout де безпечно, зменште paint-важкі ефекти.
- Запобіжники: додайте lint-правила і CI-артефакти, щоб регресії не просочувалися у п’ятницю.
- Перевірка на реальності: профілі «повільних» пристроїв, production-збірки і підтримка reduced motion.
Ваші користувачі не дбають, що ваша анімація «технічно GPU-прискорена». Їх цікавить, щоб UI реагував миттєво і скролився плавно. Будуйте під це.