Layout thrashing возникает, когда браузер вынужден многократно пересчитывать геометрию страницы из-за чередования операций чтения и записи DOM-свойств в одном кадре. На уровне движка рендеринга это приводит к постоянным reflow и repaint, которые блокируют основной поток и резко снижают плавность анимаций.
Внутренний цикл рендеринга браузера устроен так, что изменения стилей и структуры DOM группируются в отдельные фазы:
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 строится вокруг идеи минимизации работы в main thread.
Библиотека использует requestAnimationFrame и стратегию
батчинга изменений, но при неправильном использовании возможно создание
layout thrashing даже внутри анимационных цепочек.
Типичная проблема возникает при анимации свойств, зависящих от layout:
width, heighttop, left (в зависимости от position)margin, paddingЕсли в процессе анимации дополнительно считываются геометрические свойства DOM, цикл рендеринга начинает деградировать.
Ключевая стратегия устранения thrashing — разделение чтения и записи:
Пример корректного подхода:
const el = document.querySelector(".box");
// READ phase
const rect = el.getBoundingClientRect();
// WRITE phase
el.style.transform = `translateX(${rect.width}px)`;
Motion One применяет аналогичный принцип внутри своего движка, группируя изменения через очередь эффектов.
FLIP-подход (First, Last, Invert, Play) используется для анимаций, связанных с изменением layout без принудительных reflow на каждом кадре.
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 ориентирована на выполнение всех изменений в рамках одного кадра:
requestAnimationFrameКлючевая оптимизация — отказ от синхронного чтения layout в момент анимации.
import { animate } from "motion";
animate(".box",
{ x: 300 },
{ duration: 0.6 }
);
В этом случае используется transform: translateX,
который не вызывает reflow.
Даже при использовании Motion One возможны сценарии деградации производительности:
animate(".box", {
width: "400px",
x: 200
});
Изменение width требует layout recalculation, что может
нарушить оптимизированный pipeline.
animate(".box", {
x: () => document.querySelector(".box").offsetWidth
});
Каждое вычисление функции приводит к forced reflow.
Изменение одного элемента, влияющего на flow всего документа, вызывает цепную реакцию пересчётов.
Motion One использует планирование через
requestAnimationFrame для синхронизации обновлений с
кадрами рендеринга:
Псевдоструктура цикла:
queueMicrotask(() => {
// сбор изменений
});
requestAnimationFrame(() => {
// применение изменений
});
Это снижает вероятность layout thrashing до минимума при корректной работе с API.
Для предотвращения 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)`;
});
});
Здесь чтение происходит один раз, запись — пакетно.
Motion One по умолчанию опирается на свойства, которые не вызывают layout:
transformopacityЭти свойства обрабатываются на compositing уровне GPU, минуя reflow.
animate(".box", {
opacity: [0, 1],
scale: [0.8, 1]
});
Такие анимации не участвуют в layout pipeline и не создают thrashing.
При сложной композиции нескольких анимаций на одном элементе возникает риск конфликтующих вычислений:
Motion One решает это через централизованный scheduler, но при внешних DOM-операциях внутри анимационных колбэков проблема возвращается.
Дополнительный уровень защиты достигается через CSS containment:
.box {
contain: layout paint;
}
Это ограничивает влияние изменений внутри элемента на внешний DOM, уменьшая масштаб перерасчётов.
При корректной архитектуре взаимодействия с Motion One поток выглядит следующим образом:
Такой подход устраняет предпосылки для layout thrashing и сохраняет стабильную частоту кадров даже при большом количестве одновременно активных анимаций