Оптимізація CLS (Cumulative Layout Shift)
CLS — сумарна оцінка смішень елементів сторінки під час завантаження. Коли текст або кнопки стрибають у момент клік користувача — це CLS. Мета: ≤ 0,1.
Формула розрахунку
CLS = Σ (impact fraction × distance fraction) для кожного неочікуваного зміщення.
Impact fraction — частка viewport, яку займали зміщені елементи. Distance fraction — відстань зміщення відносно viewport.
Діагностика
DevTools → Performance → запис → знайти маркери «Layout Shift» (фіолетові). Або:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log('Layout shift:', entry.value, entry.sources);
}
}
}).observe({ type: 'layout-shift', buffered: true });
entry.sources показує конкретні DOM-елементи, які змістилися.
Зображення без розмірів — найчастіша причина
<!-- Погано: браузер не знає розмір до завантаження -->
<img src="product.webp" alt="...">
<!-- Добре: атрибути width та height -->
<img src="product.webp" width="800" height="600" alt="...">
CSS для адаптивності зі збереженням пропорцій:
img {
max-width: 100%;
height: auto;
}
/* Або через aspect-ratio */
.product-image {
aspect-ratio: 4 / 3;
width: 100%;
object-fit: cover;
}
Шрифти та FOUT
Font Swap викликає зміщення, коли основний шрифт завантажується та замінює системний (fallback).
/* Спосіб 1: font-display: optional — не показувати swap взагалі */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: optional;
}
/* Спосіб 2: size-adjust — привести fallback до розміру основного шрифту */
@font-face {
font-family: 'InterFallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'InterFallback', sans-serif;
}
Інструмент для підбору size-adjust: fontaine пакет або next/font (автоматично підбирає size-adjust для Google Fonts).
Динамічний контент
/* Баннер/рекламний блок: резервуйте місце до завантаження */
.ad-banner {
min-height: 90px; /* стандартний leaderboard */
background: #f5f5f5; /* placeholder */
}
/* Для адаптивних блоків */
.hero-section {
aspect-ratio: 16 / 9;
max-height: 600px;
}
Скелетони замість пустого місця
Не залишайте місце пустим — показуйте skeleton placeholder з правильними розмірами:
function ProductCardSkeleton() {
return (
<div className="product-card-skeleton">
<div className="skeleton-image" style={{ aspectRatio: '1 / 1' }} />
<div className="skeleton-title" style={{ height: '1.5rem', width: '80%' }} />
<div className="skeleton-price" style={{ height: '1.25rem', width: '40%' }} />
</div>
);
}
.skeleton-image, .skeleton-title, .skeleton-price {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Анімації
Анімації, що змінюють layout-властивості (width, height, top, left, margin, padding) викликають CLS. Використовуйте тільки transform та opacity:
/* Погано — викликає layout reflow */
.modal { animation: slideDown 300ms; }
@keyframes slideDown { from { height: 0; } to { height: 400px; } }
/* Добре — тільки transform */
.modal { animation: slideDown 300ms; }
@keyframes slideDown {
from { transform: translateY(-100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
contain: layout
Для ізольованих компонентів — contain: layout або contain: strict запобігає впливу внутрішніх змін на зовнішній layout:
.widget-container {
contain: layout;
}
Заголовок з fixed-позиціюванням
Sticky/fixed заголовок при появі не повинен смищувати контент. Padding на body:
body {
padding-top: var(--header-height, 64px);
}
.header {
position: fixed;
top: 0;
height: var(--header-height, 64px);
}
Час оптимізації: 1–3 дні, основна робота — зображення та шрифти.







