Когда вы открываете страницу, браузер не рисует её мгновенно. Между первым байтом 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, top | Layout → Paint → Composite | Дорого |
color, background, box-shadow | Paint → Composite | Средне |
transform, opacity | Composite 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 обрабатывает задачи и почему это влияет на рендер.