Что такое reflow и repaint? Как устроен пайплайн отрисовки и как писать код, не вызывающий лишних перерасчётов?
Короткий ответ
- Пайплайн кадра: style, layout, paint, composite
- Reflow (layout) — перерасчёт геометрии, самый дорогой этап
- Repaint — перерисовка пикселей без изменения геометрии
- transform и opacity обходятся одной композицией
- Чтение offsetWidth после записи стилей — принудительный синхронный layout
- Батчить чтения и записи DOM раздельно
- Анимации вести через transform, opacity и rAF
Изменения геометрии запускают дорогой reflow, визуальные свойства — repaint, а transform и opacity обрабатываются композитором почти бесплатно; главный анти-паттерн — чередование записи стилей и чтения layout-свойств (layout thrashing).
Как сказать вслух
пример ответаКогда страница меняется, браузер проходит конвейер: пересчитывает стили, затем геометрию элементов — это называется reflow, затем перерисовывает пиксели — repaint, и в конце склеивает слои на композиторе. Самое дорогое — перерасчёт геометрии, потому что изменение одного элемента может затронуть соседей. Поэтому анимации делают через transform и opacity — они обрабатываются отдельным потоком композитора и не трогают layout. И классическая ошибка — в цикле менять стили и тут же читать размеры: каждое чтение заставляет браузер пересчитать layout синхронно.
Подробный ответ
Основной ответ
Пайплайн кадра: recalc style (какие правила применяются), layout/reflow (размеры и позиции), paint (растеризация в слои), composite (сборка слоёв на GPU). Свойства по стоимости делятся на три группы: геометрические (width, top, margin, шрифты) запускают весь конвейер; визуальные (color, background, box-shadow) — paint и composite; transform и opacity у элемента на собственном слое — только composite, причём композитор работает вне главного потока, и такие анимации не дёргаются даже при занятом JS. Ключевой анти-паттерн — layout thrashing: запись стиля инвалидирует layout, а последующее чтение offsetWidth, getBoundingClientRect или scrollTop заставляет пересчитать его немедленно; в цикле это даёт десятки синхронных reflow. Решение — разделять фазы: сначала все чтения, потом все записи, изменения накапливать в классах или cssText, анимации синхронизировать через requestAnimationFrame. Дополнительно помогают content-visibility, contain и will-change (дозированно: лишние слои едят память).
Ключевые моменты
- Стоимость свойств. Геометрия — полный конвейер, краска — без layout, transform/opacity — только композиция; это определяет выбор свойств для анимаций.
- Layout thrashing. Чередование записи стилей и чтения геометрии вызывает принудительные синхронные reflow; чтения и записи батчируют раздельно.
- Композиторный поток. Анимации transform/opacity продолжают идти плавно, даже когда главный поток занят JS.
- Инструменты. DevTools Performance показывает фиолетовые блоки layout и предупреждения о forced reflow; там же видно время кадра относительно бюджета ~16 мс.
Практический контекст
Это вопрос для senior-уровня: обычно просят объяснить, почему анимация через top/left дёргается, а через transform — нет, или найти thrashing в сниппете с циклом. В работе знание применяется при анимациях, виртуализации списков, бесконечных лентах и оптимизации INP. Ожидается знакомство с профилировщиком Performance и умение связать это с метриками Core Web Vitals.
Пример кода
// Плохо: чтение после записи в цикле — reflow на каждой итерации
items.forEach(el => {
el.style.width = base + 'px';
total += el.offsetHeight; // принудительный layout
});
// Лучше: сначала все чтения, потом все записи
const heights = items.map(el => el.offsetHeight);
items.forEach(el => { el.style.width = base + 'px'; });Частые ошибки
- Анимируют width, left и margin вместо transform
- Не видят forced reflow в коде, где чтение размеров перемешано с записью стилей
- Вешают will-change на всё подряд, раздувая количество слоёв и память