В 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, генерация адаптивных вариантов плотности пикселей, прелоад через <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 (Идеально)Ключевые термины инженерии производительности:
Определяется ключевой визуальный узел. Изображению назначаются атрибуты priority и точный sizes.
Шрифты переводятся на локальную серверную раздачу с автоматической калибровкой size-adjust для CLS = 0.
В next.config.ts включается компилятор React 19 для автоматической гранулярной мемоизации компонентов.
Тяжелые трекеры аналитики выносятся из UI-потока в Partytown через next/script со стратегией worker.
Настраивается сбор Real User Monitoring (RUM) метрик для непрерывного контроля 75-го перцентиля в продакшене.
В чем заключается принципиальное отличие метрики INP от устаревшей FID и за счет чего Next.js оптимизирует ее?
next/font/google.strategy="worker", освобождающая 100% ресурсов главного потока для плавной анимации и скролла.next/image для идеального LCP#Самая распространенная причина деградации LCP — позднее обнаружение главного изображения браузером (Resource Load Delay) и скачивание избыточного объема данных.
Для вывода 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],
// Увеличение TTL кэша оптимизированных картинок на пограничных серверах
minimumCacheTTL: 31536000,
},
experimental: {
// Включение React Compiler для Next.js 15
reactCompiler: true,
},
};
export default nextConfig;Реализация Hero-компонента с точным расчетом sizes и base64 placeholder="blur":
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 или тег <link> браузер сначала отображает текст системным шрифтом (например, Arial), а затем при получении файла WOFF2 производит перерисовку (Font Swap). Поскольку метрики символов (ширина букв, высота строки, межбуквенный интервал) у 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 включен по умолчанию: генерирует точную геометрию fallback
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="ru" 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. Он статически анализирует поток данных JavaScript и автоматически внедряет мемоизацию на уровне выражений:
'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('ru-RU')} ₽</p>
</li>
))}
</ul>
</div>
);
}next/script со стратегией worker#Сторонние аналитические трекеры (Google Tag Manager, Яндекс.Метрика, Facebook Pixel) способны занять до 70% времени основного потока, разрушая показатель INP.
С помощью стратегии 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 ко всем изображениям каталога и отсутствие атрибута sizes при fill={true}?
Инженер, стремясь улучшить LCP, добавляет priority ко всем картинкам каталога (10–20 элементов). В результате браузер генерирует десятки одновременных предзагрузок <link rel="preload"> с высшим приоритетом. Сетевой канал мгновенно насыщается, и ключевой LCP-элемент первого экрана конкурирует за пропускную способность с невидимыми картинками из подвала, что приводит к двукратному росту времени загрузки.
// ❌ АНТИПАТТЕРН: Десятки картинок борются за сеть!
{products.map(p => (
<Image key={p.id} src={p.img} alt={p.name} priority={true} />
))}Решение: Выставляйте priority строго на одном главном контентном изображении первого экрана (Hero-баннер). Все остальные изображения обязаны загружаться лениво (поведение next/image по умолчанию).
При установке fill без указания свойства sizes браузер по умолчанию считает ширину контейнера равной 100vw. В результате смартфон с шириной экрана 375px и DPR 3x скачивает гигантское изображение шириной 1920px для миниатюрной карточки товара размером 120px, что многократно увеличивает время загрузки (LCP).
Решение: Всегда явно описывайте адаптивные медиазапросы в свойстве sizes, отражающие фактическую ширину блока в сетке.
Импорт шрифтов через @import url('https://fonts.googleapis.com/...'); в файле CSS создает длинный сетевой водопад: браузер сначала загружает CSS, находит @import, инициирует отдельный DNS-lookup к fonts.googleapis.com, скачивает таблицу стилей шрифтов, а затем делает еще один запрос к fonts.gstatic.com за бинарником WOFF2. Это блокирует отрисовку первого экрана и гарантирует высокий CLS.
Решение: Использовать исключительно next/font/google или next/font/local.
Размещение тяжелых маркетинговых трекеров со стратегией beforeInteractive в корневом макете. Главный поток браузера принудительно останавливает парсинг HTML до тех пор, пока внешний скрипт трекера не скачается и не выполнится. При любых сетевых сбоях на стороне трекера страница «зависает» на пустом белом экране, вызывая катастрофический рост TTFB, LCP и INP.
Решение: Переводите аналитику на стратегии worker или lazyOnload.