Пошук уроків, статей та іншого контенту
Виміряєте Web Vitals, серверну продуктивність і вузькі місця застосунку після розгортання.
Локальне вимірювання не відображає поведінку застосунку після розгортання. У production на результат впливають:
географія користувача;
тип пристрою та швидкість мережі;
кеш CDN і кеш Next.js;
холодний старт serverless-функцій;
навантаження на базу даних;
зовнішні API;
конкретна версія застосунку.
Моніторинг потрібно розділити на два рівні:
Клієнтський — що бачить користувач у браузері.
Серверний — скільки займає обробка запиту та де саме виникають затримки.
Для аналізу після розгортання важливо зберігати не лише значення метрик, а й контекст:
маршрут;
версію або commit застосунку;
регіон;
тип пристрою;
статус відповіді;
час ідентифікації запиту;
середовище (production, staging).
Web Vitals вимірюють реальний досвід користувача.
| Метрика | Що вимірює |
LCPINPCLSFCPTTFBОсновними Core Web Vitals є:
LCP: добре, якщо не більше 2.5 s;
INP: добре, якщо не більше 200 ms;
CLS: добре, якщо не більше 0.1.
Оцінювати потрібно не середнє значення, а щонайменше 75-й перцентиль (p75). Середнє може приховати проблеми окремих груп користувачів.
Наприклад, середній LCP може становити 1.8 секунди, але p75 — 4.2 секунди. Це означає, що щонайменше чверть користувачів має повільне завантаження.
У прикладі використовується Pages Router. Файл _app.tsx отримує метрики та надсилає їх на API-маршрут.
// pages/_app.tsx
import type { AppProps } from 'next/app';
import { useReportWebVitals } from 'next/web-vitals';
export default function App({ Component, pageProps }: AppProps) {
useReportWebVitals((metric) => {
const payload = JSON.stringify({
name: metric.name,
value: metric.value,
delta: metric.delta,
id: metric.id,
rating: metric.rating,
path: window.location.pathname,
release: process.env.NEXT_PUBLIC_RELEASE ?? 'unknown',
recordedAt: new Date().toISOString(),
});
if (navigator.sendBeacon) {
const body = new Blob([payload], {
type: 'application/json',
});
navigator.sendBeacon('/api/vitals', body);
return;
}
void fetch('/api/vitals', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: payload,
keepalive: true,
});
});
return <Component {...pageProps} />;
}Endpoint для приймання метрик:
// pages/api/vitals.ts
import type { NextApiRequest, NextApiResponse } from 'next';
type VitalPayload = {
name?: string;
value?: number;
delta?: number;
id?: string;
rating?: string;
path?: string;
release?: string;
recordedAt?: string;
};
const allowedMetrics = new Set(['LCP', 'INP', 'CLS', 'FCP', 'TTFB']);
export default function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST');
return res.status(405).end();
}
const body = req.body as VitalPayload;
if (
typeof body.name !== 'string' ||
!allowedMetrics.has(body.name) ||
typeof body.value !== 'number' ||
!Number.isFinite(body.value)
) {
return res.status(400).json({ error: 'Некоректна метрика' });
}
// У production тут слід передати подію до системи збору телеметрії.
console.info(
JSON.stringify({
type: 'web-vital',
name: body.name,
value: body.value,
delta: body.delta,
id: body.id,
rating: body.rating,
path: body.path,
release: body.release,
}),
);
return res.status(204).end();
}У production console.info зазвичай замінюють передаванням події до системи логування або аналітики. Важливо не зберігати в такій події:
повні URL з приватними query-параметрами;
email та інші персональні дані;
токени;
текст введених користувачем даних.
Для App Router callback потрібно викликати в клієнтському компоненті, оскільки Web Vitals вимірюються в браузері. Компонент можна підключити до кореневого layout.
// app/report-web-vitals.tsx
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function ReportWebVitals() {
useReportWebVitals((metric) => {
void fetch('/api/vitals', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
name: metric.name,
value: metric.value,
delta: metric.delta,
id: metric.id,
rating: metric.rating,
path: window.location.pathname,
}),
keepalive: true,
});
});
return null;
}Клієнтський TTFB показує весь шлях від браузера до сервера і назад. Він може включати:
DNS;
встановлення TCP-з'єднання;
TLS;
очікування CDN;
виконання Next.js;
запит до бази даних;
зовнішні HTTP-запити.
Щоб знайти серверне вузьке місце, потрібно вимірювати окремі етапи обробки запиту.
У Node.js можна використати performance.now(). Наступний API-маршрут вимірює завантаження даних і загальний час відповіді.
// pages/api/products.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { performance } from 'node:perf_hooks';
type Product = {
id: number;
name: string;
price: number;
};
async function loadProducts(): Promise<Product[]> {
// У реальному застосунку тут може бути запит до бази даних.
return [
{ id: 1, name: 'Keyboard', price: 120 },
{ id: 2, name: 'Mouse', price: 80 },
];
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const startedAt = performance.now();
const requestId =
typeof req.headers['x-request-id'] === 'string'
? req.headers['x-request-id']
: crypto.randomUUID();
try {
const dataStartedAt = performance.now();
const products = await loadProducts();
const dataDuration = performance.now() - dataStartedAt;
const totalDuration = performance.now() - startedAt;
res.setHeader(
'Server-Timing',
`data;dur=${dataDuration.toFixed(1)}, total;dur=${totalDuration.toFixed(1)}`,
);
res.setHeader('x-request-id', requestId);
console.info(
JSON.stringify({
type: 'http-request',
requestId,
method: req.method,
route: '/api/products',
statusCode: 200,
dataDurationMs: Number(dataDuration.toFixed(1)),
totalDurationMs: Number(totalDuration.toFixed(1)),
}),
);
return res.status(200).json(products);
} catch (error) {
const totalDuration = performance.now() - startedAt;
console.error(
JSON.stringify({
type: 'http-request-error',
requestId,
route: '/api/products',
totalDurationMs: Number(totalDuration.toFixed(1)),
error: error instanceof Error ? error.message : 'Невідома помилка',
}),
);
return res.status(500).json({ error: 'Внутрішня помилка сервера' });
}
}Заголовок Server-Timing можна побачити в інструментах розробника браузера. Він також дозволяє передати до браузера тривалість окремих серверних етапів.
У production лог має бути структурованим. JSON-подібний формат дозволяє фільтрувати записи за:
route;
statusCode;
requestId;
release;
totalDurationMs.
Не варто вимірювати лише загальний час. Якщо маршрут займає 800 мс, потрібно знати, де саме витрачено цей час:
500 мс — база даних;
200 мс — зовнішній API;
100 мс — серіалізація та формування відповіді.
Для HTTP-маршрутів корисно збирати:
p50 — типова затримка;
p75 — затримка, близька до досвіду більшості користувачів;
p95 — повільні запити;
p99 — найгірші типові випадки.
Наприклад, якщо p50 дорівнює 120 мс, а p95 — 4 секунди, проблема може бути не в кожному запиті, а в окремих умовах:
повільний запит до бази;
відсутній кеш;
холодний старт;
нестабільний зовнішній сервіс;
блокування ресурсів.
Відстежуйте:
частку відповідей 5xx;
частку відповідей 4xx;
помилки за маршрутом;
помилки за версією застосунку;
повторювані типи винятків;
кількість тайм-аутів.
Висока затримка без помилок також є проблемою: користувач може закрити сторінку ще до того, як сервер поверне відповідь.
Для серверного середовища важливі:
використання CPU;
використання пам'яті;
кількість активних запитів;
тривалість роботи процесу;
кількість холодних стартів для serverless;
час очікування з'єднання з базою даних;
кількість відкритих з'єднань.
Конкретний набір показників залежить від платформи розгортання та runtime. Не слід порівнювати значення CPU для довгоживучого Node.js-процесу і serverless-функції однаково.
Порівняйте:
браузерний TTFB;
серверний загальний час;
час окремих операцій;
час відповіді CDN;
час зовнішніх залежностей.
Якщо сервер обробляє запит за 100 мс, але браузер бачить TTFB 900 мс, проблема може бути в мережі, регіоні або CDN.
Якщо TTFB близький до серверного часу, досліджуйте код маршруту.
Для складного маршруту вимірюйте окремо:
перевірку автентифікації;
запит до бази даних;
запит до зовнішнього сервісу;
перетворення даних;
рендеринг або серіалізацію;
формування відповіді.
Так можна відрізнити повільний код від повільної залежності.
Логи показують окремі події, але не завжди показують повний ланцюжок запиту. Для складних застосунків потрібні distributed traces.
Один trace може містити такі операції:
HTTP GET /products 920 ms
├── database query 610 ms
├── pricing API 240 ms
└── response serialization 25 msЩоб зв'язати записи між собою, використовуйте ідентифікатор запиту або trace ID. Він має потрапляти до:
логів Next.js;
логів API;
логів доступу до бази даних;
помилок;
подій клієнтської телеметрії, якщо це безпечно.
Для Next.js застосунок можна інтегрувати з OpenTelemetry-сумісним провайдером або APM-платформою. Важливо перевірити сумісність конкретного провайдера з обраним runtime: Node.js та Edge мають різні можливості.
Метрика без версії застосунку недостатня. Після кожного розгортання порівнюйте:
p75 і p95 серверної затримки;
p75 для LCP, INP і CLS;
частку помилок;
час відповіді найповільніших маршрутів;
результати для різних регіонів.
Якщо після релізу зросли LCP і серверний час, ймовірно, проблема на сервері або в обсязі HTML. Якщо серверний час не змінився, а INP погіршився, потрібно досліджувати клієнтський JavaScript.
Перед зміною коду зафіксуйте поточний стан:
p75 Core Web Vitals;
p95 API-затримки;
частку 5xx;
навантаження CPU та пам'яті;
найповільніші маршрути.
Без базової лінії неможливо надійно визначити, чи покращився застосунок.
Алерти мають бути прив'язані до дій, які можна виконати. Наприклад:
5xx перевищує встановлений поріг протягом кількох хвилин;
p95 критичного API зростає у два рази;
p75 LCP погіршується після нового релізу;
p75 INP перевищує цільове значення;
один регіон має значно гірші показники, ніж інші.
Не створюйте алерт для кожного короткочасного стрибка. Шумні алерти швидко починають ігнорувати.
Web Vitals створюють події для багатьох користувачів. Для великих застосунків можна використовувати sampling:
збирати всі помилки;
збирати більше даних для повільних сесій;
для нормальних сесій зберігати лише частину подій;
окремо збирати дані після нового релізу.
Sampling не повинен приховувати рідкісні проблеми. Повільні значення та помилки мають потрапляти до моніторингу з високою ймовірністю.
Коли користувачі повідомляють про повільну сторінку:
Перевірте LCP, INP, CLS і TTFB для відповідного маршруту.
Розділіть дані за версією застосунку, регіоном і типом пристрою.
Перевірте серверний p95 та p99.
Знайдіть найдовший етап у логах або trace.
Порівняйте його з попереднім релізом.
Перевірте, чи не пов'язана затримка із зовнішнім сервісом або базою даних.
Внесіть одну зміну та повторіть вимірювання.
Переконайтеся, що покращення не збільшило частку помилок або навантаження на інші компоненти.
Локальний Lighthouse — це синтетичний тест у конкретних умовах. Він корисний для відтворення проблем, але не замінює дані реальних користувачів.
Середнє може приховати повільну чверть користувачів. Для Web Vitals використовуйте p75, а для серверних запитів додатково p95 і p99.
Загальна тривалість не пояснює причину. Розділяйте операції на окремі етапи та вимірюйте їх незалежно.
Без release або commit неможливо швидко пов'язати погіршення з конкретною зміною.
Не передавайте до моніторингу токени, паролі, повні query-параметри, email та вміст форм.
Не блокуйте навігацію очікуванням відповіді від системи аналітики. Використовуйте sendBeacon або fetch з keepalive.
Сторінка каталогу, API автентифікації та адміністративний звіт мають різні вимоги. Встановлюйте пороги для критичних маршрутів окремо.
Глобальне середнє може виглядати добре, хоча користувачі з певного регіону або слабких пристроїв мають значно гірший досвід.
Web Vitals показують реальний досвід користувача у браузері.
Основні Core Web Vitals — LCP, INP і CLS.
Для оцінки використовуйте перцентилі, особливо p75.
Серверні маршрути потрібно вимірювати поетапно, а не лише за загальним часом.
Логи мають бути структурованими та містити маршрут, статус, версію ідентифікатора запиту.
Distributed tracing допомагає знайти повільну базу даних або зовнішній сервіс.
Після розгортання порівнюйте метрики з базовою лінією та попереднім релізом.
Не зберігайте персональні дані у телеметрії та не створюйте алерти, які неможливо перетворити на конкретну дію.