Browser Rendering Flow

Rendering flow — процесс превращения HTML, CSS и JavaScript в пиксели на экране. Понимание этого процесса — ключ к оптимизации производительности.

Зачем нужно

Каждое изменение на странице проходит через rendering pipeline. Знание этапов позволяет понять: почему анимация тормозит, почему страница «дёргается», как избежать дорогих перерисовок. Разница между 60 FPS и 15 FPS — в понимании rendering flow.

Где используется

Оптимизация анимаций, скролла, динамического контента. Решение проблем с производительностью. Собеседования по фронтенду (частый вопрос).

Предпосылки

HTML, CSS, DOM API, Web Vitals

Полный Rendering Pipeline

HTML → Parse → DOM Tree
                        → Render Tree → Layout → Paint → Composite → Pixels
CSS  → Parse → CSSOM

Пошагово:

1. PARSING (Парсинг)
   HTML → DOM Tree
   CSS  → CSSOM Tree

2. RENDER TREE (Дерево рендеринга)
   DOM + CSSOM → Render Tree
   (только видимые элементы, без display:none)

3. LAYOUT (Разметка)
   Вычисление позиций и размеров каждого элемента.
   «Где и какого размера каждый блок?»

4. PAINT (Отрисовка)
   Заполнение пикселей: цвета, тени, текст, бордеры.
   «Как выглядит каждый пиксель?»

5. COMPOSITE (Композиция)
   Объединение слоёв в финальное изображение.
   «Наложить слои в правильном порядке.»

Этап 1: Parsing (Парсинг)

DOM Tree

<html>
  <body>
    <h1>Привет</h1>
    <p>Мир</p>
  </body>
</html>
Document
└── html
    └── body
        ├── h1
        │   └── "Привет"
        └── p
            └── "Мир"

CSSOM Tree

body { font-size: 16px; }
h1 { color: blue; }
p { display: none; }
body (font-size: 16px)
├── h1 (color: blue, font-size: 16px)
└── p (display: none, font-size: 16px)

Блокировка парсинга

<!-- CSS блокирует рендеринг (не парсинг) -->
<link rel="stylesheet" href="styles.css">

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

<!-- Решения: -->
<script src="app.js" defer></script>   <!-- Выполнить после парсинга -->
<script src="app.js" async></script>   <!-- Загрузить параллельно, выполнить сразу -->
Атрибут Загрузка Выполнение Порядок
(без) Блокирует Блокирует По порядку
async Параллельно Сразу после загрузки Не гарантирован
defer Параллельно После парсинга HTML По порядку

Этап 2: Render Tree

DOM + CSSOM → Render Tree

Render Tree содержит ТОЛЬКО видимые элементы:
✓ h1 (color: blue)
✗ p (display: none) — исключён!
✗ <head> — всегда исключён
✗ <script> — не визуальный

Этап 3: Layout (Reflow)

Браузер вычисляет геометрию: позицию (x, y) и размер (width, height) каждого элемента.

// Эти свойства ВЫЗЫВАЮТ layout (reflow):
element.offsetWidth;       // Чтение → принудительный layout
element.offsetHeight;
element.getBoundingClientRect();
window.getComputedStyle(element);

// Эти CSS-свойства ВЫЗЫВАЮТ reflow при изменении:
// width, height, margin, padding, border
// top, left, right, bottom
// font-size, line-height
// display, position, float

Forced Synchronous Layout (Layout Thrashing)

// ПЛОХО: чтение-запись-чтение-запись → layout thrashing
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
  const width = el.offsetWidth;      // Чтение → layout
  el.style.width = (width * 2) + 'px'; // Запись → invalidate
  // Следующее чтение вызовет НОВЫЙ layout!
});

// ХОРОШО: batch reads, then batch writes
const widths = ;
elements.forEach(el => {
  widths.push(el.offsetWidth); // Все чтения сначала
});
elements.forEach((el, i) => {
  el.style.width = (widths[i] * 2) + 'px'; // Все записи потом
});

Этап 4: Paint (Repaint)

Браузер заполняет пиксели: цвета, текст, тени, бордеры, изображения.

// Эти CSS-свойства вызывают ТОЛЬКО repaint (без layout):
// color, background-color, background-image
// box-shadow, border-color, outline
// visibility (НО НЕ display)

// Repaint дешевле layout, но всё ещё может тормозить

Этап 5: Composite

Браузер создаёт отдельные слои и объединяет их на GPU.

/* Эти свойства работают ТОЛЬКО на этапе composite — самые дешёвые: */
transform: translateX(100px);  /* Не вызывает layout/paint */
opacity: 0.5;                   /* Не вызывает layout/paint */

/* Для создания нового слоя: */
.animated {
  will-change: transform;  /* Подсказка браузеру: создай слой */
  /* или */
  transform: translateZ(0); /* "Хак" для GPU-ускорения */
}

Reflow vs Repaint vs Composite

Reflow (Layout):   Самый дорогой. Пересчитывает размеры и позиции.
                   Запускает: Layout → Paint → Composite

Repaint:           Средний. Перерисовывает пиксели без изменения размеров.
                   Запускает: Paint → Composite

Composite:         Самый дешёвый. Только перекладывает слои на GPU.
                   Запускает: Composite
Изменяем Что происходит Стоимость
width, height, margin Layout + Paint + Composite Высокая
color, background Paint + Composite Средняя
transform, opacity Composite only Низкая

Оптимизация анимаций

/* ПЛОХО: анимация через top/left → reflow каждый кадр */
.box {
  position: absolute;
  transition: top 0.3s, left 0.3s;
}
.box:hover {
  top: 100px;
  left: 100px;
}

/* ХОРОШО: анимация через transform → только composite */
.box {
  transition: transform 0.3s;
}
.box:hover {
  transform: translate(100px, 100px);
}
/* ПЛОХО: анимация width → reflow */
.progress {
  transition: width 0.3s;
}

/* ХОРОШО: анимация scaleX → composite */
.progress {
  transform-origin: left;
  transition: transform 0.3s;
}
.progress.fill-50 {
  transform: scaleX(0.5);
}

requestAnimationFrame

// ПЛОХО: обновление DOM в setTimeout
setTimeout(() => {
  element.style.transform = `translateX(${x}px)`;
}, 16);

// ХОРОШО: синхронизация с кадрами браузера
function animate() {
  element.style.transform = `translateX(${x}px)`;
  x += 2;
  if (x < 300) {
    requestAnimationFrame(animate);
  }
}
requestAnimationFrame(animate);

DevTools: Performance Panel

1. F12 → Performance → Record
2. Выполни действие (скролл, клик, анимация)
3. Stop → Анализ:
   - Зелёные блоки: Paint
   - Фиолетовые блоки: Layout
   - Жёлтые блоки: JavaScript
   - Красные треугольники: Long Tasks (> 50ms)

Частые ошибки

1. Layout thrashing

// Каждое чтение между записями вызывает принудительный layout
for (const el of elements) {
  el.style.height = el.offsetHeight + 10 + 'px'; // read+write в цикле!
}

2. Анимация через top/left вместо transform

/* Каждый кадр = reflow. На мобильных — 15 FPS */
.animate { transition: left 0.3s; }

/* Каждый кадр = composite. 60 FPS */
.animate { transition: transform 0.3s; }

3. Злоупотребление will-change

/* ПЛОХО: создаёт слишком много слоёв, жрёт память */
* { will-change: transform; }

/* ХОРОШО: только для элементов, которые реально анимируются */
.animated-card { will-change: transform; }

Практика

  1. Открой Performance panel в DevTools и запиши скролл страницы — найди layout/paint события
  2. Создай анимацию через top/left, потом перепиши на transform — сравни FPS
  3. Напиши пример layout thrashing и исправь его через batch reads/writes
  4. Используй will-change для ускорения анимации карточки

Связанные темы

Ресурсы