Рендеринг в браузере под нагрузкой: что ломается первым
Скролл дёргается, анимация проседает до 20 fps, а профайлер показывает жёлтые полосы в главном потоке. Разбираем путь кадра от JS до пикселей и точки, где он рвётся раньше всего.

Плавная анимация — это кадр каждые 16,6 мс при 60 Гц. На экране 120 Гц бюджет падает до 8,3 мс. В этот бюджет браузер должен уместить обработку событий, ваш JavaScript, пересчёт стилей, layout, paint и composite. Когда одна из фаз не успевает, кадр не рисуется вовсе — это и есть тот самый рывок при скролле, который пользователь чувствует, даже если не может объяснить. Разберём путь кадра в Blink (Chrome, Edge) и WebKit и посмотрим, какая фаза рвётся первой под разной нагрузкой.
Материал ориентирован на движки на базе Chromium 120+ и Safari 17. Инвалидацию кеша шрифтов и специфику WebGL здесь не трогаем — только основной pipeline HTML/CSS/DOM.
Из чего собирается кадр
Браузер проходит конвейер, который в DevTools называется rendering pipeline. Порядок фиксированный, и каждая фаза может запустить следующие:
- Input / requestAnimationFrame — обработчики событий и колбэки rAF. Ваш JS, который меняет DOM и стили.
- Style — пересчёт вычисленных стилей для затронутых элементов (recalc styles).
- Layout — вычисление геометрии: позиции и размеры. В WebKit это называется reflow.
- Pre-paint / Paint — формирование списков команд рисования по слоям.
- Composite — сборка слоёв в финальное изображение, чаще на GPU.
Ключевой факт: изменение одного свойства может задеть разное число фаз. Смена transform у элемента на своём слое доходит только до composite. Смена width запускает layout, paint и composite для всего поддерева. Отсюда и разница в цене.
Бюджет кадра — не «сколько работает ваш JS», а «сколько работает JS плюс всё, что он спровоцировал». Строка el.style.width = '200px' исполняется за микросекунды, а layout после неё — за миллисекунды.Где рвётся первым
1. Layout thrashing — самая частая причина
Layout запускается один раз на кадр — если код к этому не принуждает. Принуждает он чтением свойства, которое зависит от геометрии, сразу после записи в DOM. Браузер вынужден пересчитать layout немедленно, чтобы вернуть актуальное значение. Это принудительный синхронный layout (forced synchronous layout, он же forced reflow).
// Антипаттерн: чтение геометрии в цикле после записи
const boxes = document.querySelectorAll('.box');
for (const box of boxes) {
// запись — помечает layout как "грязный"
box.style.width = box.offsetWidth + 10 + 'px';
// чтение offsetWidth заставляет пересчитать layout прямо сейчас
}
// При 500 элементах — 500 синхронных layout вместо одногоЛечится разделением фаз чтения и записи. Сначала читаете всю геометрию, потом всё пишете:
const boxes = document.querySelectorAll('.box');
// Фаза чтения
const widths = [];
for (const box of boxes) {
widths.push(box.offsetWidth);
}
// Фаза записи
boxes.forEach((box, i) => {
box.style.width = widths[i] + 10 + 'px';
});
// Один layout на весь батчВ DevTools это видно в панели Performance как фиолетовые полосы Layout с пометкой Forced reflow is a likely performance bottleneck. Свойства, чтение которых провоцирует reflow: offsetWidth, offsetHeight, offsetTop, getBoundingClientRect(), scrollTop, getComputedStyle(). Полный список зависит от движка — его стоит держать под рукой при оптимизации.
2. Paint под большими областями
Когда меняется свойство, влияющее на внешний вид, но не на геометрию — цвет фона, тень, border-radius — layout не запускается, но paint запускается. Проблема в площади: браузер перерисовывает не элемент, а прямоугольник, покрывающий изменение. Меняете тень у контейнера на весь экран — перерисовка на весь экран.
Особенно дорого обходятся box-shadow с большим blur, filter: blur() и border-radius на анимируемых элементах. Blur — это операция на каждый пиксель области, и её стоимость растёт с площадью и радиусом размытия.
3. Composite и взрыв слоёв
Composite — самая дешёвая фаза, потому что идёт на GPU. Именно поэтому анимируют transform и opacity: они меняются только на этапе композитинга. Но у слоёв есть цена — память. Каждый слой хранится как текстура. will-change: transform на сотне элементов создаёт сотню слоёв, и на мобильном устройстве это упирается в лимит видеопамяти. Браузер начинает выгружать и перезагружать текстуры — и плавность падает сильнее, чем без оптимизации.
Что чем платит: сравнение свойств
| Что меняете | Style | Layout | Paint | Composite |
|---|---|---|---|---|
transform, opacity | да | нет | нет | да |
color, background, box-shadow | да | нет | да | да |
width, height, top, margin | да | да | да | да |
| добавление/удаление узла DOM | да | да | да | да |
Правило простое: чем правее по таблице обходится изменение, тем оно дешевле. Анимируйте transform вместо top/left — это разница между composite-only и полным проходом конвейера на каждый кадр.
Как измерять, а не гадать
Без замеров любая оптимизация — суеверие. Порядок работы в DevTools (Chromium):
- Откройте панель Performance, включите запись, воспроизведите проблемный сценарий — скролл, открытие меню, ввод в поле.
- Ищите в главном потоке (Main) длинные задачи с красным треугольником в углу — это Long Tasks дольше 50 мс.
- Смотрите на распределение по фазам внизу: если доминирует Rendering (фиолетовый) — проблема в layout/paint, если Scripting (жёлтый) — в вашем JS.
- Пометка Forced reflow прямо указывает на layout thrashing и файл со строкой, которая его вызвала.
Для paint-областей включите Rendering → Paint flashing: браузер подсветит зелёным то, что перерисовывается на каждый кадр. Если при наведении курсора мигает пол-экрана вместо одной кнопки — вот и площадь paint.
Граничные случаи, где подход ломается
- content-visibility: auto. Отключает layout и paint для элементов вне вьюпорта. Мощно на длинных списках, но ломает поиск по странице (Ctrl+F может не находить скрытое) и требует
contain-intrinsic-size, иначе скроллбар прыгает. - Слои ради оптимизации создают регрессию.
will-change, поставленный «на всякий случай» и не снятый, держит слой и память постоянно. Ставьте его перед анимацией и убирайте после. - Разница движков. Safari (WebKit) агрессивнее ограничивает число слоёв и иначе считает бюджет композитинга. То, что плавно в Chrome, может дёргаться в Safari на том же iPhone — проверяйте на целевом устройстве, а не в эмуляторе.
- Мобильный CPU. На бюджетном телефоне JS исполняется в 4–6 раз медленнее, чем на десктопе разработчика. Замеряйте с троттлингом CPU 4x в DevTools, иначе «у меня всё плавно» ничего не значит.
Что делать дальше
Начните с записи Performance на реальном сценарии и реальном устройстве — до любых правок. Определите доминирующую фазу: Scripting, Rendering или Painting. Дальше по фазе: layout thrashing лечится разделением чтения и записи, дорогой paint — сокращением площади и отказом от blur в анимации, взрыв слоёв — точечным will-change с обязательным снятием. Мерьте одну метрику — процент кадров, уложившихся в бюджет; в DevTools это виден по высоте зелёной полосы FPS. Всё остальное — детали реализации.
Инфографика для маркетплейса: Три главных преимущества товара короткими фразами, выноски в безопасной зоне превью, точный размер площадки.
Спросить за 60 ₽Источники: Rendering performance — web.dev (Chrome for Developers), Avoid large, complex layouts and layout thrashing — web.dev
Частые вопросы
У меня старый Chrome / другая версия — панель выглядит иначе?
Что если после разделения чтения и записи кадр всё равно проседает?
will-change ускоряет анимацию — почему нельзя ставить его везде?
Почему transform дешевле, чем top/left, если визуально результат тот же?
Как понять, что проблема в paint, а не в layout?
На десктопе всё плавно, а на телефоне дёргается — где искать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.