У 2026 році метрики Core Web Vitals остаточно закріпилися як суворий алгоритмічний фільтр Google Page Experience. Веб-сайти, які не потрапляють у «зелену зону» за даними звіту Chrome User Experience Report (CrUX), систематично втрачають позиції в мобільній та десктопній видачі, поступаючись конкурентам з аналогічною текстовою релевантністю.
Сучасна тріада Core Web Vitals включає:
<h1>).ДЕКОМПОЗИЦІЯ КРИТИЧНИХ МЕТРИК CORE WEB VITALS:
[ Navigation Start ]
│
├─ TTFB (Time to First Byte)
├─ Resource Load Delay (Час до початку завантаження LCP-асету)
├─ Resource Load Duration (Час завантаження AVIF/WebP)
└─ Element Render Delay (Час від завантаження до відображення в CSSOM)
│
▼
[ LCP < 2.5s ] ──► Відображення ключового контенту
│
├─ Font Swap / Size Adjust (Запобігання FOUT/FOIT)
└─ Image Aspect Ratio Reserved Space
│
▼
[ CLS < 0.1 ] ──► Стабільність верстки без зсувів
│
├─ Input Delay (Черга головного потоку перед обробкою)
├─ Processing Duration (Виконання JS-колбеків / React Render)
└─ Presentation Delay (Відмальовування кадру композитором GPU)
│
▼
[ INP < 200ms] ──► Миттєвий тактильний відгук інтерфейсуВ архітектурі Next.js 15 App Router оптимізація цих показників досягається нативною взаємодією трьох вбудованих підсистем:
next/image: Автоматична оптимізація та конвертація в AVIF/WebP, генерація адаптивних наборів srcset, попереднє завантаження через <link rel="preload"> із пріоритетом fetchpriority="high" та вбудовані base64-скелетони.next/font: Повна ізоляція веб-шрифтів на сервері (Zero External Network Requests до Google Fonts) та автоматичне калібрування метрик системних fallback-шрифтів через CSS-дескриптори size-adjust, ascent-override та descent-override.useMemo та useCallback.Оптимізація Core Web Vitals у Next.js зосереджена навколо усунення конкуренції за ресурси в критичному шляху рендерингу (Critical Rendering Path) та розвантаження основного потоку браузера (Main Thread).
БЕЗ ОПТИМІЗАЦІЇ (Типовий SPA / Старий SSR):
Main Thread: [ Parse HTML ]──[ Load GTM (Блокує) ]──[ React Hydration (Long Task 300ms!) ]──[ UI Freezes ]
Network: [ HTML ]──►[ CSS ]──►[ fonts.gstatic.com ]──►[ Lazy Image 1920px (LCP Delayed!) ]
Результат: LCP = 3.8s (Червоний), INP = 340ms (Червоний), CLS = 0.28 (Зсув тексту при swap)
З ОПТИМІЗОВАНИМ СТЕКОМ NEXT.JS 15:
Main Thread: [ Parse HTML + Shell ]──[ React Compiler (Кванти <16ms) ]──[ Free for User Input ]
Worker: └──► Partytown / next/script (worker): Виконання GTM та аналітики у Web Worker
Network: [ Preload LCP (AVIF) ] (Паралельно з HTML)
[ Inlined next/font (size-adjust) ] (Шрифт з того ж origin, CLS = 0)
Результат: LCP = 1.1s (Зелений), INP = 38ms (Зелений), CLS = 0.000 (Ідеально)Ключові терміни інженерії продуктивності:
next/font/google.strategy="worker", що звільняє 100% ресурсів процесора для інтерфейсу.next/image для ідеального LCP#Найпоширеніша причина деградації LCP — пізнє виявлення зображення браузером та завантаження надлишкових байтів.
Для виведення LCP у зелену зону конфігуруємо next.config.ts на підтримку кодека AVIF (на 30–50% ефективніший за WebP) та використовуємо next/image з прапорцем priority:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
// Пріоритетна віддача AVIF перед WebP
formats: ['image/avif', 'image/webp'],
// Розміри екранів для адаптивного srcset
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Збільшення кешування на CDN
minimumCacheTTL: 31536000,
},
experimental: {
// Увімкнення React Compiler для Next.js 15
reactCompiler: true,
},
};
export default nextConfig;Реалізація Hero-компонента з точним атрибутом sizes та розмитим плейсхолдером:
import Image from 'next/image';
interface ProductHeroImageProps {
src: string;
alt: string;
/**
* Серверний base64 blur data URL (через sharp або plaiceholder).
* Запобігає відчуттю порожнечі та усуває візуальні зсуви.
*/
blurDataUrl: string;
}
export function ProductHeroImage({ src, alt, blurDataUrl }: ProductHeroImageProps) {
return (
<div className="relative aspect-video w-full max-w-4xl overflow-hidden rounded-2xl border border-border bg-bg-surface">
<Image
src={src}
alt={alt}
fill
// КРИТИЧНО ДЛЯ LCP:
// 1. priority генерує <link rel="preload" as="image" fetchpriority="high"> у секції <head>
priority
// 2. sizes запобігає завантаженню 1920px картинки на 375px екрані смартфона
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 896px"
// 3. blur placeholder забезпечує візуальну плавність рендерингу
placeholder="blur"
blurDataURL={blurDataUrl}
className="object-cover object-center"
/>
</div>
);
}next/font та size-adjust#Під час завантаження шрифтів через @import браузер показує текст системним шрифтом, а після скачування WOFF2 замінює його. Різниця в геометрії символів між Arial та веб-шрифтом спричиняє стрибок верстки (високий CLS).
Пакет next/font вирішує це автоматично:
@font-face із дескриптором size-adjust, масштабуючи системний fallback-шрифт до розмірів кастомного:import { Inter, JetBrains_Mono } from 'next/font/google';
/**
* Основний шрифт інтерфейсу.
* next/font автоматично генерує fallback з дескрипторами size-adjust,
* повністю нейтралізуючи зміщення рядків під час завантаження тексту.
*/
export const fontSans = Inter({
subsets: ['latin', 'cyrillic'],
variable: '--font-sans',
display: 'swap',
// adjustFontFallback увімкнено за замовчуванням
adjustFontFallback: true,
preload: true,
});
/**
* Моноширинний шрифт для коду
*/
export const fontMono = JetBrains_Mono({
subsets: ['latin', 'cyrillic'],
variable: '--font-mono',
display: 'swap',
adjustFontFallback: true,
preload: true,
});Підключення шрифтів до кореневого макета:
import type { Metadata } from 'next';
import { fontSans, fontMono } from '@/app/fonts';
import '@/app/globals.css';
export const metadata: Metadata = {
title: 'Fullstack Agentic Lab Core Web Vitals Masterclass',
description: 'Високопродуктивний веб-додаток на Next.js 15',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="uk" className={`${fontSans.variable} ${fontMono.variable}`}>
<body className="bg-[#090A0F] text-slate-100 font-sans antialiased min-h-screen">
{children}
</body>
</html>
);
}Основне джерело високого INP — довгі задачі (Long Tasks > 50 мс) у головному потоці, спричинені глибокими ре-рендерами під час взаємодії користувача з інтерфейсом.
У Next.js 15 налаштування experimental: { reactCompiler: true } активує React Compiler. Він аналізує потік даних і застосовує мемоізацію автоматично:
'use client';
import { useState, useTransition } from 'react';
interface ProductItem {
id: string;
name: string;
category: string;
price: number;
}
interface DynamicFilterPanelProps {
initialItems: ProductItem[];
}
export function DynamicFilterPanel({ initialItems }: DynamicFilterPanelProps) {
const [query, setQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [isPending, startTransition] = useTransition();
// ПРИМІТКА: З React Compiler ручні хуки useMemo() та useCallback() більше НЕ ПОТРІБНІ!
// Компілятор самостійно мемоїзує операції фільтрації та результуючий JSX,
// розбиваючи код на кванти, що не блокують головний потік браузера.
const filteredItems = initialItems.filter((item) => {
const matchesQuery = item.name.toLowerCase().includes(query.toLowerCase());
const matchesCategory = selectedCategory === 'all' || item.category === selectedCategory;
return matchesQuery && matchesCategory;
});
const handleQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
// useTransition позначає оновлення списку як перериване (INP < 50ms),
// надаючи пріоритет миттєвому відгуку поля вводу.
startTransition(() => {
setQuery(val);
});
};
return (
<div className="space-y-6">
<div className="flex gap-4">
<input
type="text"
placeholder="Пошук по каталогу..."
onChange={handleQueryChange}
className="px-4 py-2 bg-bg-surface border border-border rounded-lg text-slate-200 focus:outline-none focus:border-accent-cyan"
/>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-4 py-2 bg-bg-surface border border-border rounded-lg text-slate-200"
>
<option value="all">Усі категорії</option>
<option value="servers">Сервери</option>
<option value="storage">Сховища</option>
</select>
{isPending && <span className="text-xs text-accent-cyan self-center">Фільтрація...</span>}
</div>
<ul className="grid grid-cols-3 gap-4">
{filteredItems.map((item) => (
<li key={item.id} className="p-4 rounded-lg border border-border bg-bg-surface/60">
<h4 className="font-semibold text-slate-100">{item.name}</h4>
<p className="text-sm text-text-muted mt-1">{item.price.toLocaleString('uk-UA')} ₴</p>
</li>
))}
</ul>
</div>
);
}next/script зі стратегією worker#Сторонні маркетингові скрипти здатні займати до 70% часу процесора, руйнуючи показник INP.
Використання strategy="worker" у next/script ізолює скрипти у фоновому потоці Web Worker за допомогою Partytown:
import Script from 'next/script';
/**
* Ізолює трекери у фоновому потоці Web Worker.
* Звільняє головний потік для взаємодії з інтерфейсом,
* гарантуючи показник INP < 50ms.
*/
export function AnalyticsWorkerScripts() {
return (
<>
{/* Google Tag Manager, що виконується суворо у Web Worker */}
<Script
id="gtm-worker-script"
strategy="worker"
src="https://www.googletagmanager.com/gtm.js?id=GTM-AGENTIC2026"
/>
{/* Другорядні віджети завантажуються після завершення гідратації */}
<Script
id="support-chat-widget"
strategy="lazyOnload"
src="https://widget.internal.agenticlab.dev/chat.js"
/>
</>
);
}Спроба додати priority до всіх зображень каталогу генерує десятки одночасних запитів <link rel="preload"> із найвищим пріоритетом. Мережевий канал перевантажується, і ключове LCP-зображення першого екрана починає конкурувати з другорядними картинками з підвалу.
// ❌ АНТИПАТТЕРН: Десятки зображень блокують мережу!
{products.map(p => (
<Image key={p.id} src={p.img} alt={p.name} priority={true} />
))}Вирішення: Встановлюйте priority виключно на одному головному контентному зображенні першого екрана (Hero-банері). Усі інші зображення повинні завантажуватися ледаче.
Якщо вказати fill без sizes, браузер за замовчуванням вважає ширину зображення рівною 100vw. Смартфон з екраном 375px і DPR 3x завантажує гігантське зображення шириною 1920px для маленької картки товару, катастрофічно затримуючи LCP.
Вирішення: Завжди вказуйте адаптивні медіаправила у властивості sizes відповідно до фактичної сітки верстки.
Використання @import url('https://fonts.googleapis.com/...'); у CSS створює мережевий водоспад: браузер завантажує таблицю стилів, знаходить директиву @import, виконує запит до Google Fonts, а потім окремий запит до gstatic за бінарником WOFF2. Це блокує перший рендеринг і призводить до високого CLS.
Вирішення: Використовуйте тільки next/font/google або next/font/local.
Розміщення важких трекерів аналітики зі стратегією beforeInteractive зупиняє розбір HTML браузером до повного виконання скрипта. За будь-яких мережевих затримок користувач бачить білий екран, що погіршує TTFB, LCP та INP.
Вирішення: Переводьте сторонні скрипти на стратегії worker або lazyOnload.
Визначається ключовий візуальний вузол. Зображенню призначаються атрибути priority та точний sizes.
Шрифти переводяться на локальну серверну роздачу з калібруванням size-adjust для CLS = 0.
У next.config.ts вмикається компілятор React 19 для автоматичної мемоізації компонентів.
Трекери аналітики виносяться з UI-потоку в Partytown через next/script зі стратегією worker.
Налаштовується збір Real User Monitoring (RUM) метрик для контролю 75-го перцентиля в продакшені.