← назад к разделу

Когда вы открываете страницу, браузер не рисует её мгновенно. Между первым байтом HTML и первым пикселем на экране происходит цепочка шагов — это и есть критический путь рендера (Critical Rendering Path). Если на каком-то шаге что-то тормозит, пользователь видит белый экран или скачок контента.

Понять эту цепочку полезно не ради теории. Большинство медленных первых загрузок и визуальных прыжков имеют одну из нескольких известных причин — и все они лечатся конкретными приёмами.

От байтов до пикселей

Браузер проходит несколько этапов:

HTML → DOM. Браузер читает HTML-байты и строит дерево объектов — DOM (Document Object Model). Каждый тег становится узлом дерева.

CSS → CSSOM. Параллельно со сборкой DOM браузер читает CSS и строит CSSOM (CSS Object Model). Это тоже дерево — правила и вычисленные стили для каждого узла.

Дерево рендеринга (Render Tree). DOM и CSSOM объединяются в дерево рендеринга. В него входят только видимые элементы: display: none — в дереве нет, а псевдоэлементы вроде ::before — есть.

Layout (Reflow). Браузер вычисляет геометрию — размеры и позиции каждого элемента. Это дорогая операция: изменение ширины одного блока может сдвинуть всё вокруг.

Paint. Браузер рисует пиксели — заливает цвет, применяет тени, обводки. Каждый слой рисуется отдельно.

Composite. Готовые слои отправляются на GPU, который складывает их в финальное изображение на экране.

DOM из HTML CSSOM из CSS дерево рендеринга только видимое layout размеры и позиции paint пиксели по слоям composite слои на GPU

Изменение transform запускает только последний шаг, изменение width проходит три последних заново.

Что блокирует первый рендер

CSS блокирует рендер

Браузер не построит дерево рендеринга, пока не загрузит весь CSS. Именно поэтому <link rel="stylesheet"> называется «блокирующим рендер» ресурсом. Пока стиль не загружен, пользователь видит белый экран.

Что делать: загружать только критический CSS для первого экрана; некритичные стили — асинхронно или с атрибутом media="print" и последующей сменой его значения на all.

Синхронный <script> блокирует парсинг

Встретив <script> без атрибутов, браузер останавливает сборку DOM — он должен выполнить скрипт, потому что скрипт может изменить HTML. Долгий скрипт означает долгое ожидание первого рендера.

<!-- блокирует: браузер ждёт загрузки и выполнения -->
<script src="analytics.js"></script>

<!-- defer: загружается параллельно, выполняется после парсинга HTML -->
<script src="app.js" defer></script>

<!-- async: загружается параллельно, выполняется как только загрузился -->
<script src="counter.js" async></script>

<!-- type="module": ведёт себя как defer, плюс поддержка ESM -->
<script src="main.js" type="module"></script>

Правило для большинства скриптов: используйте defer. Для независимых скриптов аналитики — async. type="module" — когда нужны ES-модули.

Шрифты и FOUT/FOIT

Браузер не показывает текст, пока не загрузит веб-шрифт. Это приводит к двум неприятностям:

  • FOIT (Flash of Invisible Text) — текст скрыт до загрузки шрифта.
  • FOUT (Flash of Unstyled Text) — текст сначала показывается системным шрифтом, потом «прыгает» на загруженный.

Управляет этим свойство font-display:

@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-display: swap; /* показать системный шрифт сразу, заменить когда загрузится */
}

font-display: swap — хороший выбор: пользователь сразу видит текст, небольшой прыжок допустим. optional — браузер использует шрифт только если успел загрузить за первые 100мс; без прыжков, но без гарантии кастомного шрифта.

Reflow, repaint и composite: что стоит дорого

Не все изменения стилей одинаково дороги:

Что меняемТриггерСтоимость
width, height, margin, topLayout → Paint → CompositeДорого
color, background, box-shadowPaint → CompositeСредне
transform, opacityComposite onlyДёшево

Когда браузер делает reflow, он пересчитывает геометрию элемента и всех, на кого это влияет. Одно изменение width может запустить перерасчёт для половины страницы. transform: translateX() двигает элемент только на GPU, не трогая геометрию — это почти бесплатно.

То же правило применяется к анимациям: анимировать transform и opacity — нормально, анимировать top или width — нет. Детально о том, как правильно строить анимации, — в статье Анимации: CSS и оптимизации.

Layout thrashing

Layout thrashing — ситуация, когда JavaScript поочерёдно читает и пишет геометрические свойства в цикле. Браузер вынужден делать reflow на каждой итерации.

// плохо: reflow на каждой итерации
const items = document.querySelectorAll('.item');
items.forEach(item => {
  const height = item.offsetHeight;    // чтение — браузер пересчитывает геометрию
  item.style.height = height * 1.5 + 'px'; // запись — аннулирует кеш геометрии
});

// хорошо: сначала все чтения, потом все записи
const heights = Array.from(items).map(item => item.offsetHeight);
items.forEach((item, i) => {
  item.style.height = heights[i] * 1.5 + 'px';
});

Правило: сначала соберите все нужные геометрические данные, потом делайте все изменения стилей. Чтение offsetHeight, getBoundingClientRect(), scrollTop после записи стиля — всегда reflow.

Как измерять

DevTools → вкладка Performance. Запишите загрузку страницы или конкретное действие. Панель показывает весь пайплайн: Loading, Scripting, Rendering, Painting — видно, где уходит время и что вызвало reflow.

Метрики LCP и CLS. LCP (Largest Contentful Paint) — когда появился главный видимый элемент. CLS (Cumulative Layout Shift) — сколько контент прыгал во время загрузки. Оба измеряются в DevTools → Lighthouse. Детально о метриках производительности React-приложений — в статье Производительность рендеринга.

Практический чек-лист быстрого первого рендера

  • <script> без defer/async вынесен или заменён на defer.
  • Критический CSS инлайнится или загружается первым, некритичный — асинхронно.
  • Шрифты используют font-display: swap или optional.
  • Тяжёлые изображения имеют атрибут loading="lazy".
  • Анимации используют transform/opacity, а не top/left.
  • Layout thrashing отсутствует: чтения геометрии не перемежаются с записью стилей.

Коротко

  • Браузер строит DOM из HTML, CSSOM из CSS, объединяет в дерево рендеринга, считает геометрию (layout), рисует слои (paint), складывает на GPU (composite).
  • CSS блокирует рендер — страница не появится, пока не загружен весь CSS.
  • <script> без атрибутов блокирует парсинг HTML; defer — загружает параллельно, выполняет после; async — выполняет сразу после загрузки.
  • font-display: swap убирает FOIT ценой небольшого FOUT; optional — без прыжков, без гарантии шрифта.
  • transform и opacity дёшевы — только composite; width/height/top дороги — запускают reflow.
  • Layout thrashing: чтение геометрии после записи стилей в цикле вызывает reflow на каждой итерации. Решение: все чтения, потом все записи.
  • LCP и CLS — ключевые метрики первого рендера; измеряются в DevTools Performance и Lighthouse.

Что почитать дальше