Как браузер рендерит страницу
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
Связанные темы
- _MOC Производительность
- will-change -- подсказка браузеру
- contain -- CSS Containment
- Core Web Vitals -- LCP, FID, CLS