← назад к разделу

Анимация — это не украшение. Хорошая анимация объясняет: кнопка «нажалась», панель «выехала», данные «загружаются». Плохая анимация — это дёрганье и скачки — признак того, что браузер не успевает уложиться в 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 и другие аспекты доступного интерфейса.