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 принудительных reflowelements.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); // все чтения — один layoutelements.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 layerel.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: разделяйте чтение и запись.