Перейти к основному содержимому

Анимации и layout

Анимации должны работать на уровне Composite — без запуска Layout и Paint. Цель — 60 FPS (~16.6ms на кадр). Любое отклонение от GPU-safe свойств вызывает reflow и деградирует INP и CLS.

Правило: анимировать только то, что обрабатывается на этапе Composite — без Layout и Paint.


GPU-safe свойства

MUST

Анимировать только GPU-accelerated свойства — они не вызывают reflow:

  • opacity
  • transform (translate, scale, rotate)

FORBIDDEN

Анимировать layout-triggering свойства — они вызывают reflow на каждом кадре:

  • top, left, right, bottom
  • width, height
  • margin, padding
/* ❌ Плохо: вызывает Layout + Paint на каждом кадре */
.card {
transition:
top 0.3s,
height 0.3s;
}

/* ✅ Хорошо: только Composite */
.card {
transition:
transform 0.3s,
opacity 0.3s;
}

Плавность и runtime-бюджет

MUST

  • 60 FPS — все визуальные обновления (анимации, скролл, переходы) выполняются с частотой 60 FPS.
  • Бюджет кадра — ~16.6ms — больше этого значения вызывает видимые подтормаживания.
  • Long Tasks < 50ms — задача в main thread не занимает больше 50ms.

Layout Thrashing

Layout thrashing возникает когда чтение и запись DOM-свойств чередуются в цикле — браузер вынужден пересчитывать layout на каждой итерации.

MUST

Разделять чтение и запись DOM-свойств: сначала все READ, потом все WRITE.

// ❌ Плохо: READ → WRITE → READ → WRITE на каждой итерации
items.forEach((el) => {
const w = el.offsetWidth; // READ — форсирует layout
el.style.width = `${w}px`; // WRITE
});

// ✅ Хорошо: все READ сначала, все WRITE потом
const widths = items.map((el) => el.offsetWidth); // все READ
items.forEach((el, i) => {
el.style.width = `${widths[i]}px`; // все WRITE
});

SHOULD

  • Использовать requestAnimationFrame для batch-обновлений DOM.

FLIP-анимации

Если изменение layout неизбежно — использовать технику FLIP (First, Last, Invert, Play) для снижения нагрузки на main thread.

// 1. First — запоминаем начальную позицию
const first = el.getBoundingClientRect();

// 2. Last — применяем изменение layout
applyLayoutChange();
const last = el.getBoundingClientRect();

// 3. Invert — вычисляем смещение
const invertX = first.left - last.left;
const invertY = first.top - last.top;

// 4. Play — анимируем через transform (GPU-safe!)
el.animate(
[
{ transform: `translate(${invertX}px, ${invertY}px)` },
{ transform: 'translate(0, 0)' },
],
{ duration: 200, easing: 'ease-out' }
);