Между получением HTML и картинкой на экране браузер проходит цепочку шагов. Её называют критическим путём рендеринга (critical rendering path). Понимая её, легко объяснить, почему страница грузится медленно и почему анимация дёргается.
Конвейер
- Парсинг HTML → DOM. Парсер читает байты по мере загрузки и строит дерево.
- Парсинг CSS → CSSOM. Все стили (внешние,
<style>, инлайн) собираются в дерево правил. - Style. Для каждого элемента вычисляются итоговые стили. Элементы с
display: noneв дальнейший рендер не попадают (в классическом описании DOM + CSSOM дают render tree). - Layout (reflow). Считаются размеры и позиции всех блоков.
- Paint. Блоки превращаются в команды рисования: фон, текст, границы, тени.
- Composite. Нарисованные слои складываются в итоговый кадр, это делает отдельный поток композитора, часто на GPU.
Что блокирует
- CSS блокирует рендер. Пока не загружен и не разобран CSS из
<head>, браузер ничего не покажет, иначе пользователь увидит вспышку нестилизованного контента. - Обычный
<script>блокирует парсинг. Парсер останавливается, ждёт загрузки и выполнения скрипта: скрипт может вызватьdocument.writeили читать DOM. А если перед скриптом стоит ещё не загруженный CSS, скрипт ждёт и его, ведь он может читать стили.
Варианты подключения скриптов:
| Атрибут | Загрузка | Выполнение | Порядок |
|---|---|---|---|
| нет | блокирует парсер | сразу | по порядку |
async | параллельно | как только скачался | любой |
defer | параллельно | после парсинга, до DOMContentLoaded | по порядку |
type="module" | параллельно | как defer по умолчанию | по порядку |
Отдельно работает preload scanner: пока главный парсер стоит на скрипте, он просматривает HTML дальше и заранее начинает качать картинки, стили и скрипты.
DOMContentLoaded и load
DOMContentLoaded- HTML разобран, DOM построен,defer-скрипты выполнены. Картинки могут ещё грузиться.load- загружено всё: картинки, стили, iframe.
Reflow, repaint, composite
Изменение стилей запускает конвейер с какого-то шага:
- Меняем геометрию (
width,top,font-size, добавили элемент) → layout, paint, composite. Самое дорогое. - Меняем только вид (
color,background,box-shadow) → paint и composite. - Меняем
transformилиopacityу элемента на своём слое → только composite. Это дёшево и не зависит от главного потока, поэтому анимации делают именно ими.
Принудительный синхронный layout
Браузер копит изменения и пересчитывает layout перед кадром. Но если после записи стиля прочитать геометрию (offsetHeight, getBoundingClientRect(), scrollTop, getComputedStyle), ему придётся посчитать layout прямо сейчас.
// плохо: чтение и запись чередуются, layout на каждой итерации
for (const el of items) {
el.style.width = box.offsetWidth + 'px';
}
// хорошо: сначала прочитали, потом записали
const w = box.offsetWidth;
for (const el of items) el.style.width = w + 'px';
Чередование чтений и записей в цикле называют layout thrashing.
requestAnimationFrame
requestAnimationFrame(cb) вызывает функцию перед следующей отрисовкой кадра. Подходит для анимаций на JS и для пакетной записи стилей. В фоновой вкладке колбэки ставятся на паузу.
Частые ошибки
- Анимировать
top/left/widthвместоtransform. - Читать
offsetHeightв цикле после изменения стилей. - Ставить тяжёлые скрипты в
<head>безdefer. - Думать, что
asyncсохраняет порядок скриптов.
Что спросят на собесе
- Этапы рендеринга страницы от HTML до пикселей.
- Чем
asyncотличается отdefer. - Reflow и repaint: что вызывает каждый и что дешевле.
- Почему анимировать
transformлучше, чемleft. - Что такое layout thrashing и как его избежать.