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

Когда вы открываете страницу, браузер не рисует её мгновенно. Между первым байтом 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, который складывает их в финальное изображение на экране.

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

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.

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

  • Производительность рендеринга — оптимизация React-компонентов, memo и code-splitting.
  • Анимации: CSS и оптимизации — как анимировать по composite-пути без reflow.
  • Event Loop и асинхронность — как JavaScript обрабатывает задачи и почему это влияет на рендер.