Анимации в CSS и JavaScript: приёмы, которые не роняют FPS
Анимация занавесилась дёргаными кадрами при скролле — знакомо. Разбираем, какие свойства анимировать дёшево, когда брать transform вместо top и почему requestAnimationFrame бьёт setInterval.

Меню выезжает рывками, при скролле список подтормаживает, а на телефоне анимация превращается в слайд-шоу. Причина почти всегда одна: анимируют не то свойство и не тем способом. Браузер отрисовывает кадры за 16,7 мс (при 60 Гц), и если вы просите его пересчитать раскладку страницы на каждом кадре — он не успевает, кадры выпадают. Разберём, какие приёмы держат анимацию на композиторе и не заставляют браузер пересчитывать layout, а где без JavaScript не обойтись.
Речь про современные браузеры 2024 года: Chromium 120+, Firefox 120+, Safari 17. Отдельные свойства вроде @property и animation-timeline поддержаны неравномерно — про это скажу отдельно.
Почему одни свойства дёшевы, а другие роняют FPS
Отрисовка кадра проходит через конвейер: пересчёт стилей → layout (раскладка, где что лежит) → paint (заливка пикселей) → composite (склейка слоёв). Чем раньше в конвейере вы дёргаете свойство, тем дороже кадр.
- Анимация
width,height,top,left,marginзапускает layout на каждом кадре. Меняется размер элемента — браузер пересчитывает положение соседей. Это самый дорогой путь. - Анимация
background-color,box-shadow,border-radiusпропускает layout, но требует paint — перезаливку пикселей. - Анимация
transformиopacityидёт только через composite. Браузер поднимает элемент в отдельный слой на GPU и двигает его, ничего не пересчитывая. Это дешёвый путь.
Практический вывод: сдвиг делайте через transform: translate(), а не через top/left; масштаб — через transform: scale(), а не через width/height; появление — через opacity, а не через display или visibility с задержкой.
Если можете выразить анимацию через transform и opacity — выражайте. Всё остальное браузер считает дороже, а на слабом телефоне разница между 60 и 24 кадрами.
Как проверить, что именно тормозит
В Chrome DevTools откройте вкладку Performance, запишите отрезок с анимацией. Фиолетовые блоки Layout и зелёные Paint во время анимации — сигнал, что вы дёргаете дорогие свойства. Если во время движения там только Composite Layers — анимация идёт на GPU.
CSS-переходы и ключевые кадры: чего хватает без JS
Большая часть интерфейсной анимации — наведение, раскрытие, появление — делается на чистом CSS. JavaScript тут только мешает.
/* Переход при наведении — transform вместо изменения размера */
.card {
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
}
.card:hover {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
/* Ключевые кадры для повторяющейся анимации */
@keyframes pulse {
from { transform: scale(1); opacity: 1; }
to { transform: scale(1.15); opacity: 0.6; }
}
.badge {
animation: pulse 1s ease-in-out infinite alternate;
}
Свойство transition подходит для реакции на изменение состояния (класс добавили — элемент плавно уехал). @keyframes — для самостоятельной анимации, которая идёт без внешнего события.
Подсказка браузеру: will-change без фанатизма
Свойство will-change: transform заранее поднимает элемент в отдельный слой, чтобы к началу анимации всё было готово. Но каждый слой — это память видеокарты. Если навешать will-change на сотню элементов, вы съедите память и получите обратный эффект. Ставьте его на конкретный анимируемый элемент и, в идеале, снимайте после окончания анимации через JS.
Когда нужен JavaScript
CSS не умеет три вещи: реагировать на произвольный ввод (позиция курсора, прогресс скролла до появления scroll-timeline), останавливать анимацию по условию в середине и связывать несколько анимаций в управляемую цепочку. Здесь вступает JS.
requestAnimationFrame вместо setInterval
Если анимируете вручную через JS — забудьте про setInterval(fn, 16). Интервал не синхронизирован с отрисовкой браузера: кадры будут то опережать, то отставать, а в фоновой вкладке таймер продолжит жечь ресурсы. requestAnimationFrame вызывается ровно перед следующей перерисовкой и засыпает, когда вкладка неактивна.
// Плавное перемещение элемента к целевой позиции
function animateTo(element, targetX, duration) {
const startX = element.getBoundingClientRect().left;
const distance = targetX - startX;
const startTime = performance.now();
function frame(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
// easeOutCubic — замедление к концу
const eased = 1 - Math.pow(1 - progress, 3);
// двигаем через transform, а не через left
element.style.transform = `translateX(${distance * eased}px)`;
if (progress < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
Обратите внимание: elapsed считается от реального времени performance.now(), а не от номера кадра. Если браузер пропустит кадр, анимация не растянется — прогресс возьмётся из времени.
Web Animations API: управляемость без ручного цикла
Если нужна CSS-подобная анимация, но с управлением из JS (пауза, реверс, отслеживание завершения), есть встроенный Element.animate(). Он работает на том же движке, что и CSS-анимации, и не нагружает главный поток покадрово.
// Web Animations API, поддержан во всех современных браузерах
const anim = panel.animate(
[
{ transform: 'translateX(100%)', opacity: 0 },
{ transform: 'translateX(0)', opacity: 1 }
],
{ duration: 300, easing: 'ease-out', fill: 'forwards' }
);
// promise завершения — можно дождаться конца анимации
anim.finished.then(() => {
console.log('панель выехала');
});
Что выбрать под задачу
| Задача | Инструмент | Почему |
|---|---|---|
| Наведение, раскрытие, смена состояния | CSS transition | Реакция на класс, ноль JS, идёт на композиторе |
| Повторяющаяся анимация без событий | CSS @keyframes | Декларативно, браузер оптимизирует сам |
| Пауза, реверс, ожидание завершения | Web Animations API | Тот же движок, но управляется из JS |
| Привязка к позиции курсора или физика | requestAnimationFrame | Произвольный ввод покадрово |
| Сложные таймлайны, morph, SVG-пути | Библиотека (GSAP и подобные) | Ручная сборка займёт больше, чем стоит |
Правило простое: не тяните JS туда, где хватает CSS, и не собирайте вручную то, что делает Element.animate().
Что ломается на практике
- Анимация до
0высоты черезheight. Классическая ошибка при раскрытии аккордеона.height: autoнельзя анимировать переходом. Обходят черезmax-heightс запасом, черезgrid-template-rows: 0fr → 1frили через измерение высоты в JS. Прямой transition сautoне сработает молча. - Забыли про
prefers-reduced-motion. Часть пользователей отключает анимацию в системе из-за вестибулярных расстройств. Оберните движение в медиазапрос:@media (prefers-reduced-motion: reduce) { * { animation: none; transition: none; } }— иначе интерфейс укачивает. - Слишком много слоёв.
will-changeиtransform: translateZ(0)на десятках элементов пожирают память GPU. Мобильный Safari от этого просто перестаёт рисовать часть страницы. - Анимация внутри обработчика scroll или resize. Эти события летят пачками. Меняйте стили не в самом обработчике, а внутри
requestAnimationFrame, иначе браузер пересчитывает layout по несколько раз за кадр (layout thrashing).
Что настроить дальше
Откройте свою анимацию в DevTools Performance и посмотрите, есть ли во время движения блоки Layout и Paint. Если есть — переведите свойства на transform/opacity. Затем добавьте медиазапрос prefers-reduced-motion. Дальше стоит присмотреться к scroll-timeline и animation-timeline — они переносят скролл-анимации в CSS без JS, но поддержка пока частичная (в Firefox и Safari на конец 2024 неполная), проверяйте на caniuse перед использованием в проде.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: MDN Web Docs — Web Animations API, web.dev — Animations and performance
Частые вопросы
Что быстрее — CSS-анимация или JavaScript?
А если у меня другая версия браузера или старый Safari?
Почему анимация не запускается, хотя класс добавляется?
Как анимировать высоту от нуля до auto?
Нужен ли will-change на все анимируемые элементы?
Как учесть пользователей, которым анимация мешает?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.