Перейти к содержимому

Rendering Optimization

Знание пайплайна рендеринга браузера (Style → Layout → Paint → Composite) позволяет целенаправленно устранять jank; разница между layout-triggering-свойствами и compositor-only-свойствами — вопрос уровня senior, напрямую влияющий на архитектуру анимаций и выбор CSS-свойств.

// Browser rendering pipeline + стоимость операций
//
// JS -> Style -> Layout (Reflow) -> Paint -> Composite
//
// Layout-triggering (дорого — вся цепочка):
// width, height, top, left, margin, padding, border
// Чтение: offsetWidth, scrollTop, getBoundingClientRect() -> принудительный layout
//
// Paint-triggering (средне — пропускает Layout):
// background-color, color, box-shadow, border-color
//
// Compositor-only (дёшево — только GPU, нет Layout и Paint):
// transform, opacity, filter (частично)
// Layout thrashing: чтение и запись геометрии вперемешку
// ❌ Плохо: N принудительных reflow
elements.forEach(el => {
const w = el.offsetWidth; // чтение → flush layout
el.style.width = w * 2 + 'px'; // запись → invalidate
// следующий offsetWidth снова форсирует layout
});
// ✅ Хорошо: batch read, then batch write (FastDOM-паттерн)
const widths = elements.map(el => el.offsetWidth); // все чтения — один layout
elements.forEach((el, i) => el.style.width = widths[i] * 2 + 'px'); // все записи
// requestAnimationFrame: синхронизация с браузерным кадром
function animate(el, from, to, duration) {
const start = performance.now();
function step(now) {
const progress = Math.min((now - start) / duration, 1);
const val = from + (to - from) * easeOutCubic(progress);
el.style.transform = `translateX(${val}px)`; // compositor-only — нет layout!
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
const easeOutCubic = t => 1 - (1 - t) ** 3;
// will-change: подсказка браузеру создать отдельный compositor layer
el.style.willChange = 'transform, opacity'; // перед анимацией
// ... после анимации освобождаем VRAM:
el.style.willChange = 'auto';
// ❌ Не злоупотреблять will-change: каждый layer = дополнительная VRAM
// Правило: применять только к элементам с реальными частыми анимациями
// CSS contain: изоляция для оптимизации layout
// contain: layout style paint — браузер не пересчитывает снаружи
// content-visibility: auto — пропустить rendering невидимых секций (iOS Safari 16+)
// Виртуализация длинных списков: рендерить только видимые строки
// Принцип windowing без библиотеки:
const ITEM_H = 48;
function renderWindow(container, items, scrollTop) {
const viewH = container.clientHeight;
const startIdx = Math.max(0, Math.floor(scrollTop / ITEM_H) - 2); // 2 строки буфер
const endIdx = Math.min(items.length, startIdx + Math.ceil(viewH / ITEM_H) + 4);
container.style.position = 'relative';
container.style.height = items.length * ITEM_H + 'px'; // виртуальная высота
const fragment = document.createDocumentFragment();
for (let i = startIdx; i < endIdx; i++) {
const div = document.createElement('div');
div.style.cssText = `position:absolute;top:${i * ITEM_H}px;height:${ITEM_H}px;width:100%`;
div.textContent = items[i].name;
fragment.appendChild(div);
}
container.replaceChildren(fragment);
}
container.addEventListener('scroll', rafThrottle(() =>
renderWindow(container, data, container.scrollTop)
), { passive: true });
// В продакшене: @tanstack/virtual (React/Vue/Solid), react-window, vue-virtual-scroller

Итог: Compositor-only свойства (transform, opacity) исключают layout и paint — основа 60fps анимаций; избегайте forced synchronous layout: разделяйте чтение и запись.