Оптимизация производительности Next.js: практическое руководство
Оптимизация производительности Next.js: практическое руководство
Производительность веб-приложений — не просто «хорошо бы иметь». Это метрика, которая влияет на конверсию, SEO и пользовательский опыт. Google использует Core Web Vitals как фактор ранжирования, а пользователи ожидают, что страница загрузится меньше чем за две секунды. Next.js даёт мощные инструменты для достижения высокой производительности, но они работают только при правильном применении.
В этом руководстве мы разберём практические техники оптимизации Next.js-приложений в 2026 году: от Server Components и работы с изображениями до стратегий кэширования и анализа бандла. Каждая техника подкреплена реальными примерами и измеримыми результатами.
Server Components: основа производительности
React Server Components (RSC) — самое значительное улучшение производительности в современном Next.js. Серверные компоненты рендерятся на сервере и отправляют в браузер готовый HTML без единой строчки JavaScript. Это означает нулевой вклад в размер клиентского бандла.
На практике большинство команд совершают одну и ту же ошибку: слишком рано добавляют директиву 'use client'. В кодовых базах, где 80% компонентов — клиентские, весь смысл RSC теряется. Каждая директива 'use client' создаёт границу гидратации. Каждая такая граница — это JavaScript, который браузер должен загрузить, распарсить и выполнить.
Правило простое: все компоненты по умолчанию серверные. Добавляйте 'use client' только когда компоненту действительно нужны браузерные API, обработчики событий, useState или useEffect. Клиентские компоненты должны быть «островками» интерактивности, встроенными в серверную обёртку.
// ✅ Правильно: серверный компонент для данных
async function ProductList() {
const products = await db.product.findMany();
return products.map(p => <ProductCard key={p.id} product={p} />);
}
// 'use client' только когда нужна интерактивность
// ✅ Маленький клиентский компонент на границе
function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => { setAdded(true); }}>
{added ? 'Добавлено' : 'В корзину'}
</button>
);
}
При аудите реального проекта с дашбордами, графиками и таблицами перенос компонентов из Client в Server Components сократил начальный JS-бандл с 400 KB до 150 KB. Логика приложения не изменилась — изменилось только то, где она выполняется.
Оптимизация изображений с next/image
Изображения составляют более 50% передаваемых байтов на большинстве страниц. Это первая и самая эффективная точка оптимизации. Компонент next/image в Next.js 15 и 16 автоматически конвертирует изображения в AVIF и WebP, генерирует responsive srcset и лениво загружает изображения ниже сгиба.
Ключевые правила работы с next/image:
- Всегда указывайте
widthиheight(илиfillс позиционированным контейнером). Без них браузер не резервирует пространство, что приводит к Cumulative Layout Shift (CLS). - Используйте
priorityдля LCP-изображения. Это добавляет<link rel="preload">в<head>и отключает ленивую загрузку для главного изображения выше сгиба. - Не ставьте
priorityбольше чем на 1–2 изображения. Иначе предзагрузка теряет смысл. - Атрибут
sizesкритически важен. Он предотвращает загрузку изображения шириной 1920 px для блока шириной 400 px. Правильныйsizesможет сократить вес изображений на 40–60%.
import Image from 'next/image';
// LCP-изображение с priority и sizes
<Image
src="/hero.webp"
alt="Hero"
width={1200}
height={600}
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
В Next.js 16 next/image использует AVIF по умолчанию и увеличил TTL кэша изображений до четырёх часов. Настройка minimumCacheTTL в конфиге позволяет управлять этим поведением:
// next.config.ts
const config: NextConfig = {
images: {
minimumCacheTTL: 86400, // кэш на сутки
},
};
Оптимизация шрифтов с next/font
Шрифты — вторая по значимости причина проблем с LCP и CLS. Неоптимизированные шрифты вызывают FOIT (Flash of Invisible Text) или FOUT (Flash of Unstyled Text), а их загрузка блокирует рендеринг.
next/font решает эти проблемы комплексно: хостит шрифты на вашем домене (нет кросс-доменных DNS-запросов), генерирует оптимальный font-display: swap и использует size-adjust, чтобы запасной шрифт визуально совпадал с основным — это устраняет layout shift.
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin', 'cyrillic'],
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html lang="ru" className={inter.className}>
<body>{children}</body>
</html>
);
}
Если вы используете Google Fonts через CSS-тег <link> в _document, вы теряете все преимущества next/font. Google Fonts с next/font загружаются со статического ресурса вашего домена, а не с googleapis.com, что устраняет лишний DNS-запрос и TLS-рукопожатие.
Кэширование: стратегии для продакшена
Система кэширования в Next.js 15 и 16 претерпела значительные изменения. Главное: ничего не кэшируется по умолчанию — вы явно указываете, что и как кэшировать. Это предсказуемо и управляемо.
Data Cache
Запросы через fetch с опцией next: { revalidate } кэшируются на уровне данных. Для сторонних клиентов (Prisma, Drizzle) используйте unstable_cache или use cache в Next.js 16:
// Next.js 16 — use cache
export default async function Page() {
const data = await getCachedData();
return <div>{data}</div>;
}
async function getCachedData() {
'use cache';
return await db.product.findMany();
}
Full Route Cache
Статические маршруты рендерятся на этапе сборки и кэшируются на CDN. Для маршрутов, которые не требуют свежих данных при каждом запросе, установите revalidate:
export const revalidate = 3600; // ревалидация раз в час
Это одна из самых простых оптимизаций TTFB. Страница генерируется один раз и отдаётся из кэша в течение часа, а TTFB падает с 500–800 ms до 50–150 ms.
ISR и on-demand ревалидация
Incremental Static Regeneration позволяет сочетать статику с динамикой. При запросе пользователь получает кэшированную версию, а Next.js в фоне перегенерирует страницу, если данные устарели:
// Server Action для инвалидации
'use server';
export async function updateProduct(id: string, data: FormData) {
await db.product.update({ where: { id }, data });
revalidatePath(`/products/${id}`);
// или по тегу:
revalidateTag('products');
}
Стриминг и параллельная загрузка данных
Самая распространённая ошибка производительности — последовательные await, которые не зависят друг от друга. Это создаёт водопады (waterfalls), где каждый следующий запрос ждёт завершения предыдущего.
Параллельная загрузка:
// ❌ Последовательно — 400 ms total
const user = await getUser(id);
const posts = await getPosts(id); // ждёт getUser
// ✅ Параллельно — 200 ms total
const [user, posts] = await Promise.all([
getUser(id),
getPosts(id),
]);
Для рендеринга используйте Suspense-границы. Они позволяют отправить shell страницы сразу, а медленные секции — стримить по мере готовности:
export default function DashboardPage() {
return (
<div>
<h1>Дашборд</h1> {/* рендерится сразу */}
<Suspense fallback={<Skeleton />}>
<SlowCharts /> {/* стримится когда готово */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<DataTable /> {/* стримится независимо */}
</Suspense>
</div>
);
}
В Next.js 16 Partial Prerendering (PPR) стабилизирован. PPR позволяет рендерить части страницы статически (статический shell), а динамические секции — стримить. Это даёт скорость статического сайта с гибкостью динамического.
Анализ и оптимизация бандла
Даже с Server Components часть JavaScript всё равно уходит на клиент. Контролировать размер бандла нужно системно.
Bundle Analyzer
Плагин @next/bundle-analyzer показывает, какие зависимости сколько весят:
ANALYZE=true npm run build
Это визуальный отчёт: вы сразу видите, что lodash весит 70 KB, а date-fns — 130 KB, при том что используется пара функций.
Dynamic Imports
Тяжёлые библиотеки — редакторы кода, графики, карты — загружайте лениво через next/dynamic. Пользователь, который не открывает редактор, не должен платить за его загрузку:
import dynamic from 'next/dynamic';
const RichTextEditor = dynamic(
() => import('@/components/RichTextEditor'),
{ loading: () => <p>Загрузка редактора...</p>, ssr: false }
);
optimizePackageImports
Настройка optimizePackageImports в next.config.ts разительно сокращает бандл для библиотек с древовидной структурой экспорта:
const config: NextConfig = {
experimental: {
optimizePackageImports: ['@iconify/react', 'lodash', 'date-fns'],
},
};
Для @iconify/react (сотни иконок) это даёт экономию в десятки килобайт.
React Compiler
Начиная с React 19, компилятор автоматически мемоизирует компоненты, устраняя необходимость в ручных useMemo и useCallback. В Next.js 16 он включается через reactCompiler: true в конфиге.
Важно: включение React Compiler на крупном проекте с существующими оптимизациями может вызвать неожиданные сайд-эффекты. Данные могут перестать обновляться, когда должны, или наоборот — рендериться слишком часто. Начинайте с малых проектов или тщательно тестируйте.
В одном из реальных кейсов отключение React Compiler дало +26 баллов PageSpeed, потому что автоматическая мемоизация конфликтовала с ручной оптимизацией, создавая избыточные рендеры. Вывод: каждую экспериментальную фичу нужно измерять на своём проекте.
Core Web Vitals: практический чек-лист
Google измеряет производительность по трём метрикам. Для статуса «хорошо» 75% реальных сессий должны проходить все три порога:
| Метрика | Хорошо | Требует улучшения | Плохо |
|---------|--------|-------------------|-------|
| LCP | ≤ 2.5 s | 2.5–4.0 s | > 4.0 s |
| INP | ≤ 200 ms | 200–500 ms | > 500 ms |
| CLS | ≤ 0.1 | 0.1–0.25 | > 0.25 |
LCP (Largest Contentful Paint)
Главная причина плохого LCP — неоптимизированное изображение выше сгиба. Решения:
next/imageсpriorityдля LCP-элемента- Правильный
sizes— чтобы мобильные устройства не загружали десктопные изображения - Хостинг шрифтов через
next/font— Google Fonts добавляет 200–400 ms к LCP
INP (Interaction to Next Paint)
INP отражает отзывчивость интерфейса. Основные причины плохого INP:
- Слишком много Client Components — каждый добавляет работу на главном потоке
- Тяжёлые библиотеки, загруженные синхронно
- Долгие задачи (long tasks) на клиенте
Решение: Server Components по умолчанию, динамический импорт тяжёлых зависимостей, Suspense-границы для стриминга.
CLS (Cumulative Layout Shift)
Layout shift возникает, когда браузер уже отрисовал элемент, а потом загрузился контент, который сдвигает его. Основные причины:
- Изображения без
width/height— браузер не зарезервировал место - Шрифты без
font-display: swapиsize-adjust— при загрузке шрифта текст меняет размер - Рекламные баннеры и инжектированный UI
Решение: next/image с размерами, next/font с display: swap, резервирование места под динамические элементы.
Заключение
Оптимизация Next.js — не набор магических настроек, а системный процесс. Каждая техника даёт измеримый результат, и при правильном подходе 90+ баллов PageSpeed достижимы для большинства приложений.
Начните с малого: аудит бандла, Server Components, next/image для всех изображений, next/font для шрифтов. Эти три шага обычно поднимают производительность с 60 до 85 баллов. Дальше — стриминг, кэширование, PPR и тонкая настройка INP.
Главное правило: измеряйте до и после. Без измеримых метрик оптимизация превращается в угадайку. Используйте next build с bundle-analyzer, PageSpeed Insights, Search Console и мониторинг Core Web Vitals в продакшене. Только данные показывают, что действительно работает для вашего проекта.