Анимация — это не украшение. Хорошая анимация объясняет: кнопка «нажалась», панель «выехала», данные «загружаются». Плохая анимация — это дёрганье и скачки — признак того, что браузер не успевает уложиться в 16 миллисекунд кадра.
Два пути: CSS и JavaScript
CSS-анимации — первый выбор для большинства задач. Браузер оптимизирует их сам.
/* transition — плавный переход при смене состояния */
.button {
transition: background-color 0.2s ease;
}
.button:hover { background-color: #1a3d34; }
/* animation — последовательность или бесконечный цикл */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.skeleton { animation: pulse 1.5s ease-in-out infinite; }
transition — когда нужно плавно перейти между двумя состояниями по событию. animation + @keyframes — для последовательностей из нескольких шагов или бесконечных циклов.
JS-анимации нужны, когда логика зависит от данных — позиции курсора, физики, динамической траектории. Правильный инструмент — requestAnimationFrame (rAF), а не setTimeout.
function animate(timestamp) {
element.style.transform = `translateX(${Math.sin(timestamp / 500) * 100}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
requestAnimationFrame синхронизируется с циклом рендера браузера — вызывается перед каждым кадром, около 60 раз в секунду. setTimeout(fn, 16) не даёт такой гарантии: он может попасть в промежуток между кадрами или задержаться из-за занятого потока. О том, как event loop управляет задачами браузера, — в статье Event Loop и асинхронность.
Главное правило производительности: transform и opacity
Браузер рисует страницу слоями. Когда вы анимируете transform или opacity, браузер двигает готовый слой на GPU — без пересчёта геометрии, без перерисовки. Это composite-only операция: быстро и дёшево.
Когда вы анимируете top, left, width — браузер делает полный цикл: пересчитывает геометрию (reflow), перерисовывает (paint), складывает слои. На каждом кадре. Отсюда дёрганье.
/* плохо: reflow на каждом кадре */
.card { transition: top 0.3s; }
.card:hover { top: -4px; }
/* хорошо: composite-only, без reflow */
.card { transition: transform 0.3s; }
.card:hover { transform: translateY(-4px); }
Почему layout дорог — в статье Критический путь рендера.
will-change: осторожно
will-change — подсказка браузеру создать GPU-слой заранее, до начала анимации:
.animated-card { will-change: transform; }
Убирает небольшой рывок при старте. Но нельзя вешать на всё подряд: каждый слой занимает память GPU — если слоёв сотни, браузер начинает тормозить. Применяйте только к элементам, которые реально анимируются, и убирайте will-change после разовых анимаций через JavaScript.
60fps и кадровый бюджет
Дисплей обновляется 60 раз в секунду — каждые ~16,7 миллисекунды новый кадр. Если JavaScript-код занял 20 миллисекунд — кадр пропущен, анимация дёрнулась. Именно поэтому тяжёлую работу не выполняют синхронно во время анимации — выносят в Web Worker или разбивают на части через requestAnimationFrame.
prefers-reduced-motion: доступность
Часть пользователей страдает от вестибулярных расстройств — интенсивные анимации вызывают у них дискомфорт. macOS, Windows и iOS позволяют включить режим «меньше движений». CSS читает этот флаг:
@media (prefers-reduced-motion: reduce) {
.hero-animation { animation: none; }
}
Или глобально — отключить все анимации через animation-duration: 0.01ms и transition-duration: 0.01ms для *. Подробнее о доступном интерфейсе — в статье Доступность (a11y).
Типовые паттерны
Появление и исчезновение
display: none не анимируется — элемент мгновенно исчезает. Используйте opacity + visibility:
.dropdown {
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
}
.dropdown.open {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
visibility: hidden убирает элемент из потока взаимодействия, но позволяет анимировать через transition — в отличие от display: none.
Скелетоны загрузки
Скользящий градиент через transform: translateX — composite-only, не нагружает CPU:
@keyframes shimmer {
from { transform: translateX(-100%); }
to { transform: translateX(100%); }
}
.skeleton-line::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
animation: shimmer 1.5s ease-in-out infinite;
}
Микровзаимодействия
Небольшие отклики интерфейса делаются через transition на :active или смену CSS-класса:
.like-button { transition: transform 0.15s ease; }
.like-button:active { transform: scale(0.9); }
.like-button.liked { transform: scale(1.15); }
Анимации в React
Проще всего — CSS-классы по состоянию компонента:
<div className={`notification ${visible ? 'notification--visible' : ''}`}>
Сохранено
</div>
.notification {
opacity: 0;
transform: translateY(8px);
transition: opacity 0.25s ease, transform 0.25s ease;
}
.notification--visible { opacity: 1; transform: translateY(0); }
Этот подход покрывает большинство случаев. Библиотека (например, Framer Motion) нужна для анимации переходов по маршруту или сложных цепочек — она работает через transform и opacity под капотом и поддерживает prefers-reduced-motion из коробки.
Коротко
- CSS
transition— для переходов по событию;animation+@keyframes— для последовательностей и бесконечных циклов. requestAnimationFrame— единственный правильный инструмент для JS-анимаций;setTimeoutне синхронизирован с кадром рендера.- Анимируйте
transformиopacity— composite-only, без reflow. Не анимируйтеtop,left,width— это layout на каждом кадре. will-change: transformсоздаёт GPU-слой заранее; вешать на всё нельзя — каждый слой занимает память GPU.- Кадровый бюджет ~16мс: тяжёлые вычисления во время анимации вызывают пропуск кадров и дёрганье.
prefers-reduced-motion: reduce— CSS-медиазапрос для отключения анимаций у пользователей с соответствующей системной настройкой.display: noneне анимируется — используйтеopacity+visibilityдля плавного появления и исчезновения.- В React: CSS-классы по состоянию покрывают большинство случаев; библиотека нужна только для сложных цепочек и переходов по маршруту.
Что почитать дальше
- Критический путь рендера — почему layout дорог и что такое composite.
- Event Loop и асинхронность — как requestAnimationFrame встраивается в цикл браузера.
- Производительность рендеринга — оптимизация React-компонентов и code-splitting.
- Доступность (a11y) — prefers-reduced-motion и другие аспекты доступного интерфейса.