Рендеринг в браузере: как получается кадр за 16 мс
Разбор пути от изменения DOM до пикселей на экране: почему layout блокирует всё, где рвётся плавность прокрутки и что именно мерить, когда страница тормозит.

С чего начинается тормозящая прокрутка
Пользователь крутит длинную ленту, и она дёргается: вместо плавных 60 кадров в секунду браузер выдаёт 20–30. В профайлере DevTools видны длинные фиолетовые полосы Layout и жёлтые Recalculate Style, каждая по 15–40 мс. Это значит, что браузер не успевает уложиться в бюджет одного кадра — при 60 Гц на всю работу отводится примерно 16,7 мс, и большую часть съедает не ваш JavaScript, а перерасчёт геометрии страницы.
Чтобы понимать, почему одни изменения дешёвые, а другие роняют FPS, нужно разобрать конвейер рендеринга: последовательность этапов, через которые проходит каждый кадр. Дальше — про Blink/WebKit-подобные движки (Chrome 120+, где актуальны названия этапов в трейсе). В Firefox с движком WebRender названия отличаются, но принцип тот же.
Конвейер: пять этапов от DOM до пикселей
Когда что-то на странице меняется — вы добавили узел, поменяли класс, проскроллили — браузер прогоняет часть или весь этот конвейер:
- Style (Recalculate Style) — движок сопоставляет CSS-правила с узлами DOM и вычисляет итоговые computed-стили для каждого элемента.
- Layout (Reflow) — по стилям считается геометрия: где элемент, какого размера, как он влияет на соседей. Это дерево бокс-моделей.
- Paint — движок записывает список команд рисования (нарисовать прямоугольник, текст, тень) для каждого слоя. Пиксели ещё не заливаются.
- Layerize / Composite — слои растеризуются в текстуры и собираются в финальную картинку. Эта работа частично уходит на GPU.
- Кадр отдаётся на экран в момент VSync.
Ключевая идея: не каждое изменение прогоняет весь конвейер. Чем ближе к концу цепочки вы вмешиваетесь, тем дешевле кадр.
Правило дешёвого кадра: анимируйте то, что композитор умеет двигать без Layout и Paint —transformиopacity. Всё остальное запускает перерасчёт.
Почему layout заразный
Layout работает не по одному элементу, а по поддереву. Изменили ширину контейнера — движок пересчитывает всех потомков, а иногда и родителей, если размер контейнера зависит от содержимого. Поэтому изменение width у одного блока в ленте способно потянуть за собой пересчёт сотен узлов.
Forced synchronous layout: главный убийца кадра
Самая частая причина непонятных фризов — принудительный синхронный layout, он же layout thrashing. Браузер откладывает layout до последнего момента: вы можете менять стили сколько угодно, и пересчёт случится один раз перед кадром. Но если вы читаете геометрию сразу после записи стиля, движок вынужден пересчитать layout немедленно, синхронно, прямо в вашем JS.
Классический антипаттерн — цикл, который в каждой итерации пишет и читает:
// Плохо: на каждой итерации forced reflow.
// offsetWidth читается сразу после записи width — layout инвалидирован.
const boxes = document.querySelectorAll('.card');
for (const box of boxes) {
box.style.width = box.offsetWidth + 10 + 'px';
}Здесь на каждой карточке чтение offsetWidth заставляет пересчитать всю геометрию заново, потому что предыдущая запись её обнулила. Сто карточек — сто полных layout вместо одного.
Лекарство — разделить фазы: сначала все чтения, потом все записи.
// Хорошо: сначала читаем, потом пишем — один layout на весь батч.
const boxes = document.querySelectorAll('.card');
const widths = [];
// фаза чтения
for (const box of boxes) {
widths.push(box.offsetWidth);
}
// фаза записи
boxes.forEach((box, i) => {
box.style.width = widths[i] + 10 + 'px';
});Какие свойства провоцируют синхронный layout
Чтение этих свойств заставляет браузер пересчитать актуальную геометрию, если стили изменялись после последнего кадра. Не заучивайте наизусть — важно знать сам факт: любое обращение к вычисленному размеру или положению может быть дорогим.
offsetWidth,offsetHeight,offsetTop,offsetLeft;clientWidth,clientHeight,scrollTop,scrollHeight;getBoundingClientRect();getComputedStyle()для свойств, зависящих от layout.
Полный перечень триггеров ведёт сообщество — искать по запросу «what forces layout reflow»; не воспринимайте любой отдельный список как исчерпывающий и вечный, набор геттеров зависит от движка и версии.
Слои композитора: где кадр становится дешёвым
Композитор работает с растеризованными слоями как с текстурами. Если слой уже нарисован, сдвинуть его через transform: translate() или изменить opacity можно без Layout и без Paint — только этап Composite. Это и есть путь к плавным анимациям.
Проблема в том, что слои стоят памяти. Каждый слой — это текстура в видеопамяти. Наплодить сотни слоёв через will-change — и вы упрётесь в память GPU, а прокрутка начнёт тормозить уже по другой причине.
| Изменение | Какие этапы запускает | Стоимость кадра |
|---|---|---|
transform, opacity (у слоя) | только Composite | низкая |
color, background, box-shadow | Paint + Composite | средняя |
width, height, top, margin | Layout + Paint + Composite | высокая |
| добавление/удаление узла DOM | Style + Layout + Paint + Composite | высокая |
will-change без фанатизма
Свойство will-change: transform подсказывает движку заранее вынести элемент в отдельный слой. Ставьте его точечно и снимайте после анимации — постоянный will-change на десятках элементов вредит: движок держит слои в памяти, даже когда анимации нет.
Где механизм ломается на практике
- Бесконечная лента без виртуализации. Тысячи узлов в DOM — и Style с Layout считаются по всему дереву даже при мелком изменении. Решение — рендерить только видимую часть (виртуальный скролл).
- Анимация через
top/left. Каждый кадр гоняет Layout. Замена наtransformубирает Layout и Paint из цикла анимации. - Библиотеки, читающие геометрию в цикле. Сторонний код, дёргающий
getBoundingClientRect()в обработчикеscroll, устраивает layout thrashing на каждый пиксель прокрутки. Ищите виновника в разделе Bottom-Up профайлера по строкам Layout. - Тяжёлый Paint из-за
box-shadowи фильтров. Большие размытые тени иfilter: blur()дороги в Paint. При скролле это заметно на слабых машинах.
Что и как мерить
Не гадайте, где узкое место — снимайте трейс. В Chrome DevTools вкладка Performance:
- Откройте Performance, нажмите запись, повторите тормозящее действие (скролл, открытие меню), остановите запись.
- Найдите красные метки Long Task и раскройте секцию Main. Длинные жёлтые блоки — ваш JS, фиолетовые — Layout, зелёные — Paint.
- Если видите Layout прямо внутри вашей JS-функции — это forced synchronous layout. DevTools помечает такие места предупреждением «Forced reflow is a likely performance bottleneck».
- Для оценки плавности включите индикатор FPS: в Rendering-панели пункт Frame Rendering Stats.
Цифры снимайте на том железе, где жалуются пользователи, а не на своём рабочем ноутбуке — разница между флагманом и бюджетным телефоном по производительности Paint легко десятикратная.
Что делать дальше
Начните с профиля реального сценария, а не с рефакторинга наугад. Если в трейсе доминирует Layout — ищите чтения геометрии в горячих обработчиках и разделяйте фазы чтения и записи. Если доминирует Paint — сокращайте тени, фильтры и площадь перерисовки. Для анимаций переводите движение на transform и opacity и выносите анимируемые элементы в слои точечным will-change. Следующий шаг для чтения — раздел про Rendering Performance в web.dev и объяснение критического пути рендеринга в документации разработчиков Chrome.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Rendering performance — web.dev, Chrome DevTools: Analyze runtime performance
Частые вопросы
У меня Firefox, а не Chrome — этот разбор подходит?
Я перевёл анимацию на transform, но всё равно тормозит. Почему?
Стоит ли ставить will-change на все анимируемые элементы заранее?
Что важнее оптимизировать — Layout или Paint?
Насколько DOM-узлов много — это уже проблема?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.