Как браузер рендерит страницу

Rendering pipeline браузера — последовательность этапов от получения HTML/CSS/JS до отображения пикселей: Parsing → Style → Layout → Paint → Compositing. Понимание pipeline объясняет, почему одни операции дорогостоящие (layout), а другие дешёвые (compositing).

Зачем нужно

Каждое изменение стиля или DOM запускает часть этого pipeline. Изменение width вызывает полный Layout+Paint+Composite; изменение transform — только Composite (GPU). Знание pipeline позволяет избегать дорогостоящих операций в анимациях и обработчиках событий.

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

  • Оптимизация анимаций (выбор между transform и top/left)
  • Понимание Layout Thrashing и как его избежать
  • Объяснение, почему requestAnimationFrame нужен для анимаций
  • CSS will-change и contain — их роль в pipeline

Основной контент

Rendering Pipeline

HTML/CSS/JS → PARSE → STYLE → LAYOUT → PAINT → COMPOSITE → Пиксели на экране

1. Parse    → Парсинг HTML в DOM, CSS в CSSOM
2. Style    → Вычисление computed styles для каждого элемента
3. Layout   → Вычисление геометрии (размер, позиция) всех элементов
4. Paint    → Рисование элементов в растровые слои (bitmap)
5. Composite → Объединение слоёв на GPU, отображение на экране

Стоимость операций

CSS property → что вызывает:

width, height, margin, padding  → Layout + Paint + Composite (дорого)
color, background-color          → Paint + Composite (средне)
transform, opacity               → только Composite (дёшево, GPU)
filter (blur, drop-shadow)       → Composite (GPU, может быть дорого)

Пример:
// ДОРОГО — вызывает Layout
element.style.width = '200px';

// ДЁШЕВО — только Composite
element.style.transform = 'translateX(100px)';

Layout Thrashing — чтение после записи

// ПЛОХО — Layout Thrashing
// Каждая пара read/write принудительно вызывает sync layout
for (const item of items) {
  const height = item.offsetHeight;          // READ  → принудительный layout
  item.style.height = (height * 1.1) + 'px'; // WRITE → invalidate layout
}
// N элементов = N принудительных layout (очень медленно)

// ХОРОШО — батчинг read/write
// Сначала все чтения
const heights = items.map(item => item.offsetHeight); // Один layout

// Потом все записи
items.forEach((item, i) => {
  item.style.height = (heights[i] * 1.1) + 'px'; // Один layout в конце
});

requestAnimationFrame — синхронизация с refresh rate

// ПЛОХО — анимация через setTimeout не синхронизирована с frame
let x = 0;
setInterval(() => {
  element.style.left = (x++) + 'px'; // Несинхронно, jank возможен
}, 16);

// ХОРОШО — rAF вызывается перед каждым frame браузера
function animate() {
  element.style.transform = `translateX(${x++}px)`;
  requestAnimationFrame(animate); // Следующий frame
}
requestAnimationFrame(animate);

Compositor Layers (GPU acceleration)

/* Свойства, создающие новый compositor layer: */
.element {
  transform: translateZ(0);    /* Классический "hack" для layer */
  will-change: transform;      /* Современный правильный способ */
  position: fixed;             /* Fixed elements = собственный layer */
  /* iframe, video, canvas — всегда на отдельном layer */
}

/* Каждый layer = память GPU. Не создавайте лишних! */

Critical Rendering Path

HTML → DOM
CSS  → CSSOM
          ↓
     Render Tree (DOM + CSSOM)
          ↓
        Layout
          ↓
         Paint
          ↓
       Composite

Оптимизация Critical Path:
  - Минимизировать CSS блокирующий рендер
  - Async/defer JavaScript
  - Inline Critical CSS
  - Preload ключевых ресурсов

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

  • Анимация top/left вместо transform: translate — вызывает layout
  • Чтение layout properties (offsetHeight, getBoundingClientRect) внутри циклов записи
  • Создание сотен compositor layers через will-change — исчерпание GPU памяти
  • Тяжёлые вычисления в scroll/resize handlers без throttle — блокируют main thread

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

Ресурсы