CSS vs JS-анимации: скорость, удобство и выбор

Есть CSS и JS анимация. Какая между ними разница, что быстрее, что более удобно?

В чём разница CSS и JS

CSS-анимации (transition/animation) описывают «что должно меняться» (ключевые кадры, длительность, easing), а браузер решает «как именно» встроить это в рендеринг и какие оптимизации применить.

JS-анимации чаще означают ручное обновление значений во времени (например, через requestAnimationFrame(...)), то есть логика вычисления каждого кадра выполняется на main thread и конкурирует с прочими задачами JavaScript (обработчики событий, логика приложения, парсинг данных).

Web Animations API (WAAPI) находится между этими подходами: анимация задаётся из JavaScript, но исполняется как «движковая» анимация, поэтому при подходящих свойствах может оптимизироваться похожим образом, как CSS-анимации.

Почему одни анимации «быстрые»

Браузер формирует кадр через этапы: пересчёт стилей (recalculate style) → layout (геометрия) → paint (рисование пикселей) → composition/composite (сборка слоёв).

Если изменение свойства требует layout, то затем обычно потребуется и paint, а это быстро «съедает» бюджет кадра и приводит к рывкам, особенно на страницах со сложной версткой и большим количеством элементов.

Если изменение можно выполнить на этапе композиции (часто так бывает с transform и opacity, когда элемент вынесен в отдельный слой), то можно избежать layout и paint в каждом кадре и получить более стабильную плавность.

Схема (упрощённо)

JS/CSS меняет значения
        |
Recalculate style
        |
Layout (reflow) — пересчёт размеров/позиций
        |
Paint — перерисовка пикселей
        |
Composite — сборка слоёв и вывод на экран

Таблица стоимости свойств

Категория измененияЧто обычно происходитПримеры свойствЧто это значит для анимации
Layout + Paint + CompositeТребуется пересчитать геометрию и затем перерисоватьwidth, height, left, margin, font-sizeЧасто самая дорогая анимация, особенно на сложных страницах
Paint + CompositeГеометрия не меняется, но нужны новые пикселиcolor, box-shadow (часто дорогой paint)Может быть приемлемо для маленьких областей, но на больших — тяжело
Composite (иногда)Можно обойтись сборкой слоёв без перерисовкиtransform, opacityОбычно лучший вариант для плавного движения/прозрачности
Свойство will-change способно помочь браузеру подготовить оптимизации под будущую анимацию, но избыточное применение может тратить память, увеличивать количество слоёв и в итоге ухудшать производительность.

Что удобнее в практике

CSS-анимации часто проще и короче по записи для типовых эффектов (появление, смещение, hover-переходы), их легче поддерживать, когда логики мало и важна декларативность: достаточно описать состояния и длительность.

JS/WAAPI удобнее, когда требуется управление временем и состоянием: поставить на паузу, продолжить, синхронизировать несколько эффектов, вычислять параметры на основе данных, связывать прогресс анимации с прокруткой или состоянием приложения.

С точки зрения производительности нельзя считать «CSS всегда быстрее»: если анимируется «дорогое» свойство, то main thread будет занят layout/paint независимо от того, задано это в CSS или из JS.

Код и диагностика

Ниже приведены примеры: «дешёвый» путь через transform/opacity и «дорогой» путь через свойства, затрагивающие геометрию, чтобы было видно, почему вариант 1 считается корректным.

// CSS: перемещение через transform (обычно дешевле)

.box {
  will-change: transform, opacity;
  animation: move 300ms ease-out forwards;
}

@keyframes move {
  from { transform: translateX(0); opacity: 0.7; }
  to   { transform: translateX(200px); opacity: 1; }
}
// CSS: более дорогое изменение геометрии (часто требует layout + paint)

.box {
  animation: grow 300ms ease-out forwards;
}

@keyframes grow {
  from { width: 120px; }
  to   { width: 320px; }
}
// JS + requestAnimationFrame: ручной расчёт прогресса и обновление transform

const box = document.querySelector('.box');

const DURATION = 300;
const DIST = 200;

let start = null;

function tick(t) {
  if (start === null) start = t;

  const elapsed = t - start;
  const p = Math.min(elapsed / DURATION, 1);

  box.style.transform = `translateX(${DIST * p}px)`;
  box.style.opacity = String(0.7 + 0.3 * p);

  if (p < 1) requestAnimationFrame(tick);
}

requestAnimationFrame(tick);
// WAAPI: анимация из JS, но исполняется «движком анимаций» браузера

const box = document.querySelector('.box');

box.animate(
  [
    { transform: 'translateX(0px)', opacity: 0.7 },
    { transform: 'translateX(200px)', opacity: 1 }
  ],
  {
    duration: 300,
    easing: 'ease-out',
    fill: 'forwards'
  }
);

Для диагностики производительности полезно записывать профили в Performance-инструментах браузера и смотреть, какие этапы доминируют по времени (layout, paint, composite) и насколько стабилен fps.

Если во время анимации видны частые и дорогие layout/paint, то даже «идеальный» таймер (CSS, WAAPI или requestAnimationFrame(...)) не исправит ситуацию, потому что основная стоимость уходит на перерасчёт и перерисовку.

Итого: корректным является вариант 1, потому что плавность определяется «ценой» анимируемых свойств и загрузкой main thread; варианты 2–4 неверны, так как CSS не гарантирует одинаковую скорость для любых свойств, JS не является «всегда быстрее», а удобство зависит от сценария (типовая анимация или логика и управление).