Reflow и repaint: почему интерфейс тормозит и как это чинить
Разбор того, что заставляет браузер пересчитывать геометрию и перерисовывать страницу, как измерить лаги в DevTools и какие паттерны кода бьют по кадровой частоте.

Список из тысячи строк дёргается при скролле, а плавная на десктопе анимация превращается в слайд-шоу на среднем Android. Причина чаще не в тяжёлом JS, а в том, что браузер по вашей вине пересчитывает раскладку по несколько раз за кадр. У вас есть 16,7 мс на кадр при 60 fps — и один неудачный доступ к offsetHeight в цикле способен съесть их целиком. Разберём, что именно запускает reflow и repaint, как это увидеть в DevTools и какие приёмы возвращают анимации в бюджет кадра.
Что такое reflow и repaint
Браузер строит картинку из нескольких стадий. Упрощённо: пересчёт стилей → раскладка (layout) → отрисовка (paint) → композиция (composite). Reflow — это повторный расчёт геометрии: где и какого размера каждый элемент. Repaint — перерисовка пикселей без изменения геометрии, например при смене background-color.
Ключевое отличие в цене. Repaint дорог, но локален. Reflow тянет за собой пересчёт потомков, а часто и предков и соседей — и почти всегда завершается новым repaint. Поэтому первая цель оптимизации — убрать лишние reflow, вторая — сократить площадь repaint.
Самый дорогой reflow — тот, который вы вызвали случайно и несколько раз за один кадр, сами того не заметив.
Что запускает reflow
- Изменение размеров окна или шрифтов.
- Добавление, удаление или перемещение DOM-узлов.
- Изменение геометрических свойств:
width,height,margin,padding,top,left,font-size,display. - Чтение свойств, которые заставляют браузер синхронно посчитать раскладку:
offsetTop,offsetHeight,clientWidth,scrollTop,getBoundingClientRect(),getComputedStyle().
Что обходится дешевле
Свойства transform и opacity при правильных условиях обрабатываются на стадии композиции — без reflow и без repaint основного слоя. Именно поэтому анимацию сдвига делают через transform: translateX(), а не через left.
Layout thrashing: главный источник тормозов
Форсированный синхронный reflow (его называют layout thrashing) возникает, когда вы в одном цикле чередуете запись в DOM и чтение геометрии. Браузер кеширует изменения и применяет их пачкой, но как только вы запрашиваете актуальное значение размера, ему приходится немедленно пересчитать раскладку, чтобы отдать правильное число.
Классический антипаттерн — выровнять высоту блоков по самому высокому в цикле:
// ПЛОХО: чтение offsetHeight после каждой записи width
// Каждая итерация форсирует reflow
const boxes = document.querySelectorAll('.card');
for (const box of boxes) {
box.style.width = '200px'; // запись — раскладка помечена «грязной»
const h = box.offsetHeight; // чтение — форсированный reflow
box.style.height = h + 'px'; // снова запись
}
Здесь на каждой карточке браузер пересчитывает раскладку заново. При сотне элементов это сотня reflow вместо одного.
Лечится разделением фаз: сначала все чтения, потом все записи.
// ХОРОШО: batch-чтение, затем batch-запись
const boxes = document.querySelectorAll('.card');
// фаза 1 — только читаем
const heights = [...boxes].map((box) => box.offsetHeight);
// фаза 2 — только пишем
boxes.forEach((box, i) => {
box.style.width = '200px';
box.style.height = heights[i] + 'px';
});
Теперь браузер посчитает раскладку один раз перед фазой чтения и один раз применит все записи. Проверьте в DevTools: в первом варианте в панели Performance будет частокол фиолетовых блоков Layout, во втором — один-два.
requestAnimationFrame и разделение чтения/записи
В сложных сценариях чтения и записи разбросаны по разным функциям. Тогда помогает планирование через requestAnimationFrame: измерения делают в одном коллбэке, мутации — в следующем. Библиотека FastDOM реализует ровно эту идею — очередь read и write. Если тянуть зависимость не хочется, тот же приём пишется руками: собирайте все нужные размеры в один проход, потом применяйте изменения.
Как измерить, а не гадать
Не оптимизируйте вслепую. В Chrome DevTools откройте вкладку Performance, запишите взаимодействие (скролл, анимацию, ввод) и смотрите на треки:
- Recalculate Style — пересчёт стилей.
- Layout (фиолетовый) — reflow. Множество мелких блоков подряд в одном кадре — признак layout thrashing.
- Paint (зелёный) — repaint. Смотрите на площадь.
- Красный флажок на кадре — кадр вышел за бюджет, пользователь увидит рывок.
Границы перерисовки можно подсветить: в DevTools откройте Rendering (через меню More tools) и включите Paint flashing — области repaint замигают зелёным. Если при наведении курсора мигает пол-экрана вместо кнопки — вы перерисовываете слишком много.
Есть и API замера прямо в коде: обёртка performance.now() вокруг блока, который читает и пишет DOM, покажет реальную стоимость в миллисекундах на конкретном устройстве. Синтетика на топовом ноутбуке врёт — мерьте на том, чем пользуются ваши люди.
Приёмы, которые реально помогают
1. Анимируйте transform и opacity
Сдвиг, масштаб, поворот и прозрачность браузер умеет отдавать на композитный слой. Меняйте transform: translate3d(x, 0, 0) вместо left, transform: scale() вместо width/height. Свойство will-change: transform подсказывает браузеру заранее выделить слой — но не вешайте его на всё подряд, каждый слой ест видеопамять.
2. Меняйте классы, а не стили по одному
Вместо десяти присваиваний element.style.* переключите один класс. Браузер применит все изменения одной пачкой, а не будет реагировать на каждое свойство.
3. Стройте DOM вне документа
При массовой вставке узлов собирайте их в DocumentFragment и вставляйте одним appendChild. Изменения фрагмента не в дереве документа reflow не вызывают.
// Одна вставка вместо N reflow
const fragment = document.createDocumentFragment();
for (let i = 0; i < 500; i++) {
const li = document.createElement('li');
li.textContent = 'Строка ' + i;
fragment.appendChild(li);
}
document.querySelector('#list').appendChild(fragment); // единственное касание живого DOM
4. Изолируйте перерисовку через CSS contain
Свойство contain: layout paint говорит браузеру, что содержимое элемента не влияет на раскладку остальной страницы — reflow внутри не выйдет за его границы. Для длинных списков и виджетов, живущих независимо, это ощутимо сокращает область пересчёта. Поддержка есть во всех актуальных браузерах, но проверьте покрытие под свою аудиторию на caniuse.
5. content-visibility для длинных страниц
Свойство content-visibility: auto позволяет браузеру пропускать раскладку и отрисовку контента за пределами вьюпорта. На длинных лентах это заметно ускоряет первую отрисовку. Задавайте contain-intrinsic-size, иначе полоса прокрутки будет прыгать по мере рендеринга.
Что выбирать под задачу
| Задача | Плохо | Хорошо |
|---|---|---|
| Плавный сдвиг элемента | анимация left/top | transform: translate |
| Показать/скрыть блок с fade | анимация display/height | opacity + visibility |
| Вставить много узлов | appendChild в цикле | DocumentFragment |
| Выровнять высоты | чтение offsetHeight в цикле с записью | batch: сначала читать, потом писать |
| Длинная лента | рендерить всё сразу | content-visibility или виртуализация |
Где приёмы ломаются
Композитный слой не бесплатен. Сотни элементов с will-change или transform: translate3d раздувают потребление видеопамяти, и на слабом мобильном GPU вы получите не ускорение, а вылеты вкладки. Правило: слои для того, что реально анимируется прямо сейчас, а не про запас.
content-visibility: auto ускоряет рендер, но ломает нативный поиск по странице (Ctrl+F не находит скрытый контент в части браузеров) и якорные ссылки на невидимые блоки. Взвесьте, критично ли это для вашего интерфейса.
И главное: transform не участвует в раскладке, значит соседние элементы его сдвиг не заметят. Если вам нужно, чтобы соседи подвинулись, обойтись композицией не получится — придётся платить за reflow осознанно.
Что делать дальше
Начните с замера: запишите Performance-профиль проблемного взаимодействия и найдите кадры с красным флажком и частоколом Layout. Дальше по порядку — уберите чтение геометрии из циклов записи, переведите анимации на transform/opacity, включите Paint flashing и убедитесь, что перерисовывается только нужная область. Для очень длинных списков смотрите в сторону виртуализации (react-window, virtua) — это отдельная большая тема. Контрольный показатель в проде — доля кадров дольше 16,7 мс во время скролла и анимаций.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: web.dev — Avoid large, complex layouts and layout thrashing, MDN — CSS containment (contain, content-visibility)
Частые вопросы
Чем reflow отличается от repaint простыми словами?
Почему transform быстрее, чем изменение left?
У меня не сработало — анимация transform всё равно тормозит. Что смотреть?
Стоит ли ставить will-change на всё, что анимируется?
Эти приёмы одинаково работают во всех браузерах?
Как понять, что проблема именно в reflow, а не в тяжёлом JS?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.