Layout thrashing

Layout thrashing возникает, когда браузер вынужден многократно пересчитывать геометрию страницы из-за чередования операций чтения и записи DOM-свойств в одном кадре. На уровне движка рендеринга это приводит к постоянным reflow и repaint, которые блокируют основной поток и резко снижают плавность анимаций.

Внутренний цикл рендеринга браузера устроен так, что изменения стилей и структуры DOM группируются в отдельные фазы:

  • вычисление стилей (style recalculation)
  • построение layout (reflow)
  • отрисовка (paint)
  • компоновка слоёв (composite)

Layout thrashing появляется в момент, когда код нарушает естественную пакетную обработку этих фаз:

const el = document.querySelector(".box");

el.style.width = "200px";           // запись
const w = el.offsetWidth;           // чтение → принудительный reflow
el.style.height = w + "px";         // запись
const h = el.offsetHeight;          // чтение → ещё один reflow

Каждое обращение к layout-свойствам (offsetWidth, offsetHeight, getBoundingClientRect) заставляет браузер синхронно пересчитывать геометрию, даже если до этого были только изменения стилей.

Влияние на анимации в Motion One

Motion One строится вокруг идеи минимизации работы в main thread. Библиотека использует requestAnimationFrame и стратегию батчинга изменений, но при неправильном использовании возможно создание layout thrashing даже внутри анимационных цепочек.

Типичная проблема возникает при анимации свойств, зависящих от layout:

  • width, height
  • top, left (в зависимости от position)
  • margin, padding
  • любые вычисляемые через layout значения

Если в процессе анимации дополнительно считываются геометрические свойства DOM, цикл рендеринга начинает деградировать.

Принцип разделения read/write фаз

Ключевая стратегия устранения thrashing — разделение чтения и записи:

  1. Сначала выполняются все чтения layout
  2. Затем выполняются все изменения стилей
  3. После этого происходит один layout pass

Пример корректного подхода:

const el = document.querySelector(".box");

// READ phase
const rect = el.getBoundingClientRect();

// WRITE phase
el.style.transform = `translateX(${rect.width}px)`;

Motion One применяет аналогичный принцип внутри своего движка, группируя изменения через очередь эффектов.

FLIP как основной инструмент оптимизации

FLIP-подход (First, Last, Invert, Play) используется для анимаций, связанных с изменением layout без принудительных reflow на каждом кадре.

  • First: фиксируется начальная позиция
  • Last: вычисляется конечная позиция
  • Invert: рассчитывается разница
  • Play: анимация через transform
const el = document.querySelector(".item");

const first = el.getBoundingClientRect();

el.classList.add("expanded");

const last = el.getBoundingClientRect();

const deltaX = first.left - last.left;
const deltaY = first.top - last.top;

el.animate([
  { transform: `translate(${deltaX}px, ${deltaY}px)` },
  { transform: "translate(0, 0)" }
], {
  duration: 300,
  easing: "ease-out"
});

Motion One использует аналогичную концепцию при переходах между состояниями, избегая прямой работы с layout во время анимации.

Как Motion One минимизирует layout thrashing

Архитектура Motion One ориентирована на выполнение всех изменений в рамках одного кадра:

  • все анимационные обновления собираются в очередь
  • вычисления выполняются внутри requestAnimationFrame
  • DOM обновляется пакетно
  • transform и opacity приоритетны, так как не вызывают layout

Ключевая оптимизация — отказ от синхронного чтения layout в момент анимации.

import { animate } from "motion";

animate(".box", 
  { x: 300 },
  { duration: 0.6 }
);

В этом случае используется transform: translateX, который не вызывает reflow.

Скрытые источники layout thrashing в анимациях

Даже при использовании Motion One возможны сценарии деградации производительности:

1. Смешивание transform и layout-свойств

animate(".box", {
  width: "400px",
  x: 200
});

Изменение width требует layout recalculation, что может нарушить оптимизированный pipeline.

2. Чтение DOM внутри animation loop

animate(".box", {
  x: () => document.querySelector(".box").offsetWidth
});

Каждое вычисление функции приводит к forced reflow.

3. Каскадные изменения зависимых элементов

Изменение одного элемента, влияющего на flow всего документа, вызывает цепную реакцию пересчётов.

Батчинг изменений и роль requestAnimationFrame

Motion One использует планирование через requestAnimationFrame для синхронизации обновлений с кадрами рендеринга:

  • изменения собираются в микротаске
  • выполняются перед paint
  • браузер получает один consolidated layout pass

Псевдоструктура цикла:

queueMicrotask(() => {
  // сбор изменений
});

requestAnimationFrame(() => {
  // применение изменений
});

Это снижает вероятность layout thrashing до минимума при корректной работе с API.

Практика изоляции layout-зависимых вычислений

Для предотвращения thrashing вычисления геометрии выносятся за пределы анимационного цикла:

const boxes = document.querySelectorAll(".box");

const widths = Array.from(boxes).map(el => el.offsetWidth);

requestAnimationFrame(() => {
  boxes.forEach((el, i) => {
    el.style.transform = `translateX(${widths[i]}px)`;
  });
});

Здесь чтение происходит один раз, запись — пакетно.

Transform как основа производительных анимаций

Motion One по умолчанию опирается на свойства, которые не вызывают layout:

  • transform
  • opacity

Эти свойства обрабатываются на compositing уровне GPU, минуя reflow.

animate(".box", {
  opacity: [0, 1],
  scale: [0.8, 1]
});

Такие анимации не участвуют в layout pipeline и не создают thrashing.

Композиция анимаций и потенциальные узкие места

При сложной композиции нескольких анимаций на одном элементе возникает риск конфликтующих вычислений:

  • одновременное изменение layout и transform
  • пересекающиеся эффекты нескольких timeline
  • синхронные измерения DOM между keyframes

Motion One решает это через централизованный scheduler, но при внешних DOM-операциях внутри анимационных колбэков проблема возвращается.

Изоляция layout через containment

Дополнительный уровень защиты достигается через CSS containment:

.box {
  contain: layout paint;
}

Это ограничивает влияние изменений внутри элемента на внешний DOM, уменьшая масштаб перерасчётов.

Итоговая модель поведения рендеринга

При корректной архитектуре взаимодействия с Motion One поток выглядит следующим образом:

  • вычисления состояния выполняются вне render phase
  • изменения группируются в один frame
  • layout не блокируется чтениями в write phase
  • анимации выполняются через compositing слой

Такой подход устраняет предпосылки для layout thrashing и сохраняет стабильную частоту кадров даже при большом количестве одновременно активных анимаций