Пошук уроків, статей та іншого контенту
Розберете LCP, CLS та INP, їхні причини погіршення й способи вимірювання в реальному застосунку.
LCP, CLS та INP — це Core Web Vitals, тобто основні користувацькі метрики продуктивності вебсторінки.
Вони описують три різні характеристики:
LCP (Largest Contentful Paint) — наскільки швидко з’являється найбільший видимий елемент основного вмісту.
CLS (Cumulative Layout Shift) — наскільки сильно зміщується розмітка під час завантаження.
INP (Interaction to Next Paint) — наскільки швидко сторінка реагує на взаємодію користувача.
Оцінки зазвичай аналізують на 75-му перцентилі. Це означає, що щонайменше 75% вимірювань мають бути не гіршими за відповідне значення.
| Метрика | Добре | Потребує покращення | Погано | |---|---:|---:|---:| | LCP | до 2,5 с | 2,5–4 с | понад 4 с | | CLS | до 0,1 | 0,1–0,25 | понад 0,25 | | INP | до 200 мс | 200–500 мс | понад 500 мс |
LCP фіксує момент, коли в області перегляду відображається найбільший елемент основного вмісту. Найчастіше ним є:
велике зображення-обкладинка;
заголовок сторінки;
великий текстовий блок;
банер або відеообкладинка.
LCP не обов’язково є останнім елементом, який завантажився. Браузер може оновлювати кандидат на LCP у процесі завантаження сторінки.
Типові причини:
повільна відповідь сервера;
довгий час до отримання першого байта HTML;
блокувальні CSS або JavaScript;
велике зображення без оптимізації;
завантаження LCP-зображення лише після виконання JavaScript;
повільний шрифт, якщо заголовок приховується до його завантаження;
надмірна кількість клієнтського JavaScript;
запити до зовнішніх сервісів перед показом основного вмісту.
Для Next.js важливо, де формується LCP-вміст. Якщо основний контент можна отримати на сервері, не варто змушувати браузер чекати на гідрацію клієнтського компонента.
Для сторінок, де дані можна отримати на сервері, використовуйте серверні компоненти та серверний fetch. Не переносьте отримання критичних даних у useEffect, якщо через це користувач спочатку бачить порожній екран.
// app/products/page.tsx
type Product = {
id: number;
name: string;
};
async function getProducts(): Promise<Product[]> {
const response = await fetch('https://api.example.com/products', {
next: { revalidate: 60 },
});
if (!response.ok) {
throw new Error('Не вдалося завантажити товари');
}
return response.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Каталог товарів</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</main>
);
}У цьому прикладі серверний компонент отримує дані до формування HTML. Браузер може почати показувати заголовок і список без очікування виконання клієнтського JavaScript.
Компонент next/image допомагає генерувати відповідний розмір зображення та оптимізувати його доставку. Для LCP-зображення також потрібно вказати розміри або співвідношення сторін.
import Image from 'next/image';
export default function Hero() {
return (
<section>
<Image
src="/images/hero.jpg"
alt="Команда розробників за роботою"
width={1600}
height={900}
sizes="100vw"
style={{ width: '100%', height: 'auto' }}
priority
/>
<h1>Створюйте швидкі вебзастосунки</h1>
</section>
);
}priority повідомляє Next.js, що зображення важливе для початкового відображення. Його слід використовувати для справжнього LCP-зображення, а не для всіх зображень сторінки.
Не використовуйте loading="lazy" для зображення, яке має стати LCP. Відкладене завантаження потрібне для зображень, розташованих нижче початкової області перегляду.
Якщо текстовий LCP-елемент залежить від вебшрифту, налаштуйте його завантаження так, щоб браузер міг показати текст без тривалого невидимого стану.
Також варто:
використовувати сучасні формати шрифтів;
завантажувати лише потрібні накреслення;
не підключати десятки шрифтів на одній сторінці;
перевіряти, чи справді шрифт потрібен для початкового екрана.
CLS вимірює неочікувані зміщення видимих елементів під час життя сторінки.
Наприклад, користувач натискає кнопку, але в цей момент над нею завантажився банер і кнопка перемістилася вниз. Користувач фактично натиснув уже інший елемент.
Найчастіші причини:
зображення без width і height;
відео або iframe без зарезервованого місця;
рекламні блоки, які вставляються після завантаження;
динамічні повідомлення над уже видимим контентом;
пізня вставка шрифтів із суттєво іншими розмірами символів;
клієнтський компонент, який після гідрації змінює структуру сторінки;
анімація через властивості top, left, width або height.
next/image із заданими розмірами зазвичай створює необхідне співвідношення сторін і не дає контенту стрибати після завантаження зображення.
Для адаптивних блоків можна явно вказати співвідношення сторін:
import Image from 'next/image';
export default function ArticleCover() {
return (
<div style={{ position: 'relative', aspectRatio: '16 / 9' }}>
<Image
src="/images/article-cover.jpg"
alt="Обкладинка статті"
fill
sizes="(max-width: 768px) 100vw, 768px"
style={{ objectFit: 'cover' }}
/>
</div>
);
}Для відео, iframe та рекламних місць використовуйте контейнер із визначеною висотою або aspect-ratio.
Якщо повідомлення або банер може з’явитися після завантаження, заздалегідь виділіть для нього місце. Ще краще — розміщуйте такі елементи поверх контенту, якщо це відповідає дизайну, наприклад через фіксований контейнер.
Не слід додавати панель у верхню частину сторінки після того, як користувач уже почав читати контент.
Скелетон має мати приблизно ті самі розміри, що й готовий компонент. Якщо скелетон має висоту 80 пікселів, а завантажений блок — 400, після отримання даних відбудеться зміщення.
export function ProductCardSkeleton() {
return (
<article
aria-hidden="true"
style={{
minHeight: 280,
borderRadius: 8,
background: '#e5e7eb',
}}
/>
);
}INP вимірює затримку між взаємодією користувача та наступним оновленням відображення.
Взаємодіями можуть бути:
натискання кнопки;
введення тексту;
вибір пункту меню;
перемикання вкладки;
відкриття модального вікна.
На сторінці може бути багато взаємодій. INP оцінює найгірші або майже найгірші затримки, тому одна повільна кнопка може погіршити загальний результат.
Після взаємодії браузер може витратити час на:
очікування звільнення головного потоку;
виконання обробника події;
перерахунок стилів і layout;
малювання наступного кадру.
Наприклад, якщо обробник кліку виконує складний цикл або обробляє великий масив даних, браузер не може швидко показати результат.
великий обсяг JavaScript, який потрібно завантажити та виконати;
важка гідрація клієнтських компонентів;
довгі завдання на головному потоці;
синхронні обчислення в обробниках подій;
повторне відображення великого дерева компонентів;
оновлення стану, яке змінює надто велику частину сторінки;
виконання сторонніх скриптів у момент взаємодії.
залишайте компонент клієнтським лише тоді, коли йому потрібна інтерактивність;
розділяйте великі клієнтські компоненти на менші;
не виконуйте важкі обчислення безпосередньо в обробнику події;
оновлюйте лише потрібну частину інтерфейсу;
відкладайте другорядну роботу після того, як браузер покаже основний результат;
перевіряйте довгі завдання у Performance-профайлері браузера.
Наприклад, після натискання користувачу достатньо одразу показати результат, а другорядне логування можна виконати пізніше:
'use client';
import { useState } from 'react';
export default function LikeButton() {
const [liked, setLiked] = useState(false);
function handleClick() {
setLiked((current) => !current);
// Другорядну роботу не слід виконувати до оновлення інтерфейсу.
setTimeout(() => {
console.log('Стан реакції можна синхронізувати з аналітикою');
}, 0);
}
return (
<button type="button" onClick={handleClick}>
{liked ? 'Вам подобається' : 'Подобається'}
</button>
);
}Цей приклад не робить важку роботу швидкою сам по собі. Він демонструє принцип: критичне для інтерфейсу оновлення не варто блокувати другорядними діями.
Є два основні види вимірювання:
лабораторне — у контрольованих умовах за допомогою Lighthouse або Performance-профайлера;
польове — у реальних користувачів, браузерах, мережах і пристроях.
Лабораторні тести зручні для пошуку причин. Польові дані показують, як застосунок працює насправді.
Одна й та сама сторінка може мати добрий результат на потужному комп’ютері розробника та поганий результат на мобільному пристрої в повільній мережі.
web-vitalsДля збору метрик у браузері можна використати пакет web-vitals:
npm install web-vitalsСтворимо клієнтський компонент, який підписується на метрики та надсилає їх на API-застосунку.
// app/WebVitals.tsx
'use client';
import { useEffect } from 'react';
import { onCLS, onINP, onLCP, type Metric } from 'web-vitals';
function sendMetric(metric: Metric) {
const payload = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
rating: metric.rating,
path: window.location.pathname,
});
const body = new Blob([payload], {
type: 'application/json',
});
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/web-vitals', body);
return;
}
fetch('/api/web-vitals', {
method: 'POST',
body: payload,
headers: {
'Content-Type': 'application/json',
},
keepalive: true,
}).catch(() => {
// Помилка аналітики не повинна ламати роботу сторінки.
});
}
export default function WebVitals() {
useEffect(() => {
onCLS(sendMetric);
onINP(sendMetric);
onLCP(sendMetric);
}, []);
return null;
}Підключимо компонент у кореневому layout:
// app/layout.tsx
import type { ReactNode } from 'react';
import WebVitals from './WebVitals';
export default function RootLayout({
children,
}: {
children: ReactNode;
}) {
return (
<html lang="uk">
<body>
<WebVitals />
{children}
</body>
</html>
);
}Для App Router endpoint можна створити як Route Handler:
// app/api/web-vitals/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
const metric = await request.json();
if (
typeof metric.name !== 'string' ||
typeof metric.value !== 'number'
) {
return NextResponse.json(
{ error: 'Некоректна метрика' },
{ status: 400 },
);
}
// У реальному застосунку тут зберігають дані в системі аналітики.
console.log({
name: metric.name,
value: metric.value,
rating: metric.rating,
path: metric.path,
});
return NextResponse.json({ ok: true });
} catch {
return NextResponse.json(
{ error: 'Некоректне тіло запиту' },
{ status: 400 },
);
}
}Такий приклад можна запустити локально. У production замість console.log зазвичай використовують власне сховище або систему аналітики.
Під час збору польових даних варто:
не надсилати персональні дані без потреби;
не записувати URL із конфіденційними параметрами;
валідовувати дані на сервері;
за потреби використовувати вибірку, а не відправляти кожне вимірювання;
групувати результати за сторінкою, пристроєм, браузером і версією застосунку.
LCP може уточнюватися протягом завантаження сторінки. Бібліотека web-vitals враховує життєвий цикл сторінки та зазвичай надсилає фінальне значення, коли користувач залишає сторінку або вона переходить у прихований стан.
Тому не слід очікувати, що LCP буде відправлено одразу після першого показу елемента.
Одного числового значення недостатньо. Якщо LCP або INP погіршився, потрібно встановити, що саме створює затримку.
Перевірте:
який елемент став LCP;
чи це зображення, текст або інший блок;
час відповіді сервера;
час завантаження ресурсу;
чи не завантажується ресурс після JavaScript;
чи не блокує його CSS або шрифт.
Перевірте:
який елемент змістився;
чи має зображення зарезервований розмір;
чи з’являються пізні банери або повідомлення;
чи змінюється розмітка після гідрації;
чи не анімуються layout-властивості.
Перевірте:
яка саме взаємодія була повільною;
який обробник події її обробляє;
чи були довгі завдання на головному потоці;
скільки компонентів повторно відрендерилося;
чи виконується сторонній JavaScript.
У браузері Chrome ці причини можна досліджувати через вкладку Performance і режим аналізу взаємодій. Lighthouse корисний для першої перевірки, але його результат не замінює дані реальних користувачів.
Потужний процесор, швидкий SSD і стабільний інтернет приховують проблеми, які помітні на мобільних пристроях.
next/image автоматично вирішує всі проблеми LCPКомпонент оптимізує зображення, але неправильний розмір, зайвий lazy loading, повільний сервер або блокувальний JavaScript усе одно можуть погіршити LCP.
priority усім зображеннямПріоритет має бути лише в ресурсів, потрібних для початкового екрана. Якщо зробити пріоритетними всі зображення, браузер отримує забагато конкуруючих завантажень.
Будь-яке зображення, відео, iframe або динамічний блок може спричинити зміщення, якщо його розмір не відомий заздалегідь.
Сторінка може швидко показати заголовок і мати добрий LCP, але погано реагувати на клік через важкий JavaScript. LCP та INP вимірюють різні етапи взаємодії із сайтом.
Результати залежать від умов тесту. Для стабільного висновку потрібні повторні лабораторні вимірювання та польові дані.
Endpoint для метрик доступний із браузера, тому його дані не можна безумовно вважати коректними. На сервері потрібно перевіряти типи, діапазони значень і формат запитів.
LCP показує, наскільки швидко користувач бачить основний вміст.
CLS показує стабільність розмітки під час завантаження.
INP показує швидкість реакції сторінки на взаємодії.
Для добрих оцінок потрібно оптимізувати не лише ресурси, а й серверний HTML, клієнтський JavaScript, розміри медіа та динамічні блоки.
Lighthouse і DevTools допомагають знаходити причини в контрольованих умовах.
web-vitals дає змогу збирати польові дані з реальних браузерів.
Оцінювати продуктивність потрібно за групами користувачів і на 75-му перцентилі, а не за одним вимірюванням.