Критический путь отрисовки: что тормозит первый экран
Браузер не рисует страницу построчно сверху вниз. Между получением HTML и первым пикселем на экране лежит цепочка из пяти этапов, и любой из них может задержать отрисовку на сотни миллисекунд.

Страница весит 300 КБ, сеть быстрая, а первый экран появляется через 2,5 секунды. В DevTools видно, что HTML пришёл за 200 мс, но First Contentful Paint случился только на 2,4 секунды. Между этими двумя точками браузер прошёл критический путь отрисовки — Critical Rendering Path, CRP. Разбираем, из чего он состоит, где чаще всего застревает и что именно измерять, чтобы найти виновника.
Речь про поведение движков на базе Blink (Chrome, Edge, Opera) и WebKit (Safari); в Gecko (Firefox) этапы те же, отличаются названия внутренних структур. Замеры и панели ниже — из Chrome DevTools версии 2024 года.
Пять этапов от байтов до пикселей
Критический путь — это последовательность преобразований, которую браузер проходит, прежде чем показать хоть что-то. Порядок фиксированный, и каждый следующий этап зависит от результата предыдущего.
- Построение DOM. Парсер читает HTML побайтово, разбивает на токены, из токенов собирает узлы и строит дерево DOM. Парсинг инкрементальный: браузер начинает строить дерево, не дожидаясь конца документа.
- Построение CSSOM. Параллельно из CSS собирается объектная модель стилей. В отличие от DOM, CSSOM строится не инкрементально: пока не загружен и не разобран весь CSS, объектная модель считается неполной.
- Render Tree. DOM и CSSOM сливаются в дерево отрисовки. В него попадают только видимые узлы: элементы с
display: noneв дерево не входят, аvisibility: hidden— входит, потому что занимает место. - Layout (reflow). Браузер вычисляет геометрию: координаты и размеры каждого узла с учётом viewport. На выходе — коробочная модель с точными пикселями.
- Paint и Composite. Узлы растеризуются в слои, слои композятся в финальную картинку, которую видит пользователь.
Ключевая точка: пока не готовы и DOM, и CSSOM, Render Tree построить нельзя. Значит, CSS блокирует первую отрисовку — даже если весь HTML уже распарсен.
Почему CSS блокирует отрисовку, а скрипты — парсинг
CSS браузер считает render-blocking ресурсом. Логика простая: если начать рисовать до готовности стилей, пользователь увидит нестилизованный контент, а потом резкий скачок при применении CSS — то самое мелькание FOUC. Поэтому браузер придерживает первую отрисовку, пока не разберёт CSS, участвующий в текущем медиа-контексте.
Скрипты ведут себя иначе — и опаснее. Обычный <script> без атрибутов блокирует парсер HTML: встретив тег, браузер останавливает построение DOM, скачивает скрипт, исполняет его, и только потом продолжает читать документ. Причина в том, что скрипт может вызвать document.write и переписать поток.
Ещё коварнее связка: если перед синхронным скриптом стоит внешний CSS, браузер задержит исполнение скрипта до готовности CSSOM. Скрипт может читать вычисленные стили через getComputedStyle, поэтому движок не рискует запустить его раньше. Получается цепочка: CSS блокирует скрипт, скрипт блокирует парсинг DOM, DOM блокирует Render Tree.
Синхронный скрипт в <head>, за которым тянется тяжёлый CSS, — это не одна задержка, а произведение двух. Чаще всего именно эта комбинация даёт двухсекундный белый экран на среднем сайте.
Preload scanner: почему не всё так плохо
Пока основной парсер стоит на синхронном скрипте, работает вспомогательный поток — preload scanner1. Он забегает вперёд по сырому HTML и находит ссылки на ресурсы (картинки, CSS, скрипты), запуская их загрузку раньше, чем до них доберётся основной парсер. Именно поэтому загрузка не встаёт полностью. Но preload scanner видит только то, что уже пришло в HTML: ресурсы, добавленные через JS или через @import внутри CSS, он не находит и загрузит поздно.
Что измерять и где
Абстрактные советы «оптимизируйте CRP» бесполезны без замеров. Открываете вкладку Performance в DevTools, записываете загрузку и смотрите на конкретные полосы.
| Что видно в Performance | Какой этап CRP | О чём говорит |
|---|---|---|
| Длинная полоса Parse HTML с разрывами | Построение DOM | Парсер прерывался синхронными скриптами |
| Recalculate Style | DOM + CSSOM → Render Tree | Сложные или объёмные селекторы |
| Layout, помеченный красным | Reflow | Принудительная синхронная перекомпоновка |
| Paint и Composite Layers | Растеризация | Много слоёв или тяжёлые эффекты |
Отдельно смотрите метрику FCP в панели Lighthouse или в разделе Timings панели Performance — это момент, когда на экране появился первый значимый контент. Разница между приходом HTML и FCP — и есть ваш критический путь в миллисекундах.
Forced reflow — тихий убийца кадров
Отдельная ловушка: чтение геометрии сразу после её изменения из JS. Браузер вынужден пересчитать Layout прямо в момент чтения, синхронно, ломая свою же оптимизацию отложенного reflow.
// Плохо: чередование записи и чтения в цикле
// каждое чтение offsetHeight форсирует reflow
const items = document.querySelectorAll('.card');
for (const item of items) {
const height = item.offsetHeight; // чтение — форсирует Layout
item.style.height = (height * 2) + 'px'; // запись — инвалидирует Layout
}
// Хорошо: сначала все чтения, потом все записи
const heights = [];
for (const item of items) {
heights.push(item.offsetHeight); // фаза чтения
}
items.forEach((item, i) => {
item.style.height = (heights[i] * 2) + 'px'; // фаза записи
});
В первом варианте на 500 карточек браузер выполнит 500 синхронных пересчётов Layout. Во втором — один. Свойства, которые форсируют reflow при чтении: offsetTop, offsetHeight, scrollTop, getComputedStyle, getBoundingClientRect. Полный список поддерживают в открытом gist комьюнити, но принцип один: любое чтение вычисленной геометрии после записи — красный флаг.
Три рычага, которые реально двигают FCP
Оптимизация CRP — это не бесконечный список микротюнинга, а работа с тремя величинами: сколько ресурсов блокируют отрисовку, сколько байтов в каждом, сколько шагов в цепочке зависимостей.
- Убрать блокирующие ресурсы с критического пути. Скрипты, которые не нужны для первого экрана, помечайте атрибутом
defer(исполнится после парсинга DOM, с сохранением порядка) илиasync(исполнится сразу после загрузки, порядок не гарантирован). CSS, который не нужен на первом экране, грузите черезmedia-запрос — тогда браузер не считает его render-blocking для текущего вьюпорта. - Сократить объём критического CSS. Инлайновый критический CSS в
<head>убирает сетевой круг для стилей первого экрана. Остальное подгружается асинхронно. Инструменты для выделения критического CSS есть, но проверяйте результат глазами: автоматика иногда режет нужное. - Уменьшить глубину зависимостей.
@importвнутри CSS создаёт последовательную цепочку: браузер сначала грузит основной файл, разбирает, находит import, только потом грузит следующий. Preload scanner такое не видит. Замените на несколько<link>в HTML — они загрузятся параллельно.
Где эта картина ломается
Описанная линейная цепочка верна для первой загрузки классической многостраничной верстки. В нескольких сценариях она работает иначе:
- SPA после первого рендера. Дальше в игру вступает виртуальный DOM или сигналы фреймворка, и узкое место смещается с сети на исполнение JS и точечные перерисовки. Критический путь первой загрузки при этом никуда не девается — просто добавляется второй фронт работ.
- Composite-only анимации. Свойства
transformиopacityпри аппаратном ускорении меняются на этапе Composite, минуя Layout и Paint. Поэтому анимация черезtransform: translateXплавная, а черезleft— дёргается: последняя каждый кадр форсирует reflow. - Streaming HTML и приоритеты. Современные серверы отдают HTML потоком, а браузер начинает парсить первый чанк, не дожидаясь конца. Схема «сначала весь HTML, потом DOM» здесь неточна — DOM строится по мере поступления байтов.
Что делать дальше
Начните с одного замера: запишите загрузку в Performance на throttling-профиле Slow 4G и найдите расстояние между Parse HTML и FCP. Если между ними разрывы — ищите синхронные скрипты. Если долгий Recalculate Style — смотрите объём и селекторы CSS. Дальше стоит настроить непрерывный мониторинг FCP и LCP на реальных пользователях через Web Vitals, потому что лабораторные замеры на вашей машине оптимистичнее полевых. И держите в голове главную зависимость: пока CSSOM не готов, первого пикселя не будет — значит, критический CSS всегда в приоритете.
Видеомейкер: Ролик по описанию или оживление вашего фото. От пяти секунд, со звуком, вертикально или горизонтально.
Спросить за 190 ₽Источники: Critical rendering path — web.dev, Populating the page: how browsers work — MDN
Частые вопросы
Чем defer отличается от async, если оба не блокируют парсер?
У меня Firefox, а не Chrome — этапы будут те же?
Я вынес весь CSS в асинхронную загрузку, но появился FOUC — что не так?
Как понять, что виноват именно forced reflow, а не сеть?
Стоит ли оптимизировать CRP, если сайт открывается быстро на моём ноутбуке?
@import в CSS действительно так вреден?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.