Kodokon kodokon.com

Конвейер рендеринга: reflow, repaint, композитинг

Разберись в четырёх фазах отрисовки в браузере, чтобы точно знать, какие свойства CSS обходятся дорого и как will-change работает изнутри.

11 мин · 3 вопросов

Открыть этот урок в Kodokon

Браузер превращает твой CSS в пиксели через конвейер из четырёх фаз: Style (вычисление значений), Layout (геометрия, он же reflow), Paint (растеризация в пиксели, тот самый repaint) и Composite (сборка слоёв на GPU). Золотое правило: чем более ранней фазы ты касаешься, тем больше счёт, потому что каждая следующая фаза проигрывается заново. Изменение width или top запускает layout + paint + composite. Изменение color или box-shadow пропускает layout, но перерисовывает. Только transform и opacity может обработать один поток композитора: анимация остаётся плавной, даже когда основной поток заблокирован JavaScript.

CSS
.slide-bad {
  position: absolute;
  transition: left 0.3s ease;
}
.slide-bad:hover { left: 120px; }

.slide-good {
  transition: transform 0.3s ease;
}
.slide-good:hover { transform: translateX(120px); }
Две визуально одинаковые анимации: первая заставляет делать reflow на каждом кадре, вторая остаётся на композиторе.

Браузер ленив: он копит твои записи стилей и пересчитывает layout только к следующему обновлению кадра. Но некоторые чтения из JavaScript - offsetHeight, getBoundingClientRect(), scrollTop, getComputedStyle() по геометрическому свойству - требуют актуальных значений. Если запись ещё не применена, браузер обязан пересчитать всё немедленно и синхронно: это и есть принудительный reflow. Чередование чтений и записей внутри цикла порождает layout thrashing: один полный reflow на каждую итерацию вместо одного на весь пакет.

JAVASCRIPT
const items = document.querySelectorAll('.item');

items.forEach((item) => {
  const h = item.offsetHeight;
  item.style.height = h * 2 + 'px';
});

const heights = [...items].map((i) => i.offsetHeight);
items.forEach((item, index) => {
  item.style.height = heights[index] * 2 + 'px';
});
Вариант 1: по одному принудительному reflow на элемент. Вариант 2: сначала все чтения, потом все записи - один-единственный reflow.

Композитинг нарезает страницу на слои, которые растеризуются независимо и собираются на GPU. will-change: transform просит поднять элемент в собственный слой до анимации, избавляя от дорогой растеризации на первом кадре. Тонкость спецификации, о которой часто забывают: will-change обязан давать те же побочные эффекты, что и свойство, которое он анонсирует. Поэтому will-change: transform создаёт контекст наложения и превращает элемент в содержащий блок для его потомков с position: fixed - ровно как настоящий transform. Простая подсказка о производительности может, таким образом, сломать твой порядок наложения или твои фиксированные элементы.

JAVASCRIPT
const panel = document.querySelector('.panel');

panel.addEventListener('pointerenter', () => {
  panel.style.willChange = 'transform';
});

panel.addEventListener('transitionend', () => {
  panel.style.willChange = 'auto';
});
Паттерн, рекомендованный спецификацией: поднимать слой прямо перед анимацией и отпускать сразу после.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Какая анимация может быть полностью обработана потоком композитора, без reflow и без repaint?
    • height с 0 до 200px
    • margin-left с 0 до 100px
    • transform: translateX(100px)
    • box-shadow при наведении
  2. Почему чтение offsetHeight внутри цикла, который заодно меняет стили, обходится так дорого?
    • Каждое чтение вызывает синхронный reflow, если есть неприменённая запись
    • offsetHeight запускает repaint на GPU при каждом обращении
    • Это свойство никогда не кэшируется движком JavaScript
  3. Какой побочный эффект will-change: transform разделяет с настоящим transform?
    • Он отключает CSS-переходы элемента
    • Он создаёт контекст наложения и содержащий блок для position: fixed
    • Он принудительно переводит элемент в display: block
    • Он удваивает разрешение растеризации