Next.js умеет рендерить страницы на сервере, но за этой фразой скрывается несколько разных режимов: статическая генерация на сборке, серверный рендеринг по запросу, инкрементальная ревалидация и потоковая отдача через Suspense. Ошибка в выборе режима превращается в медленный ответ или устаревшие данные. Ниже — практический разбор App Router: как получить данные, где включается кэш и как проверить, что страница действительно рендерится так, как задумано.
Как устроен App Router
В App Router компоненты по умолчанию серверные. Они выполняются на сервере, могут обращаться к базе и файловой системе, не попадают в клиентский бандл. Клиентские компоненты помечаются директивой 'use client' и нужны для состояния, эффектов и обработчиков событий.
Страница — это файл page.tsx в сегменте маршрута. Рядом могут лежать layout.tsx, loading.tsx, error.tsx и not-found.tsx. Такая раскладка задаёт поведение без ручной настройки роутера.
Создание проекта
Для нового проекта достаточно генератора. Он спросит про TypeScript, ESLint, Tailwind и App Router — для примера ниже нужен именно App Router.
npx create-next-app@latest shop
cd shop
npm run dev
npm run build
Проверка сборки важнее, чем кажется: часть ошибок — обращение к window, неверные типы параметров, динамические API в статическом сегменте — проявляется только на build, а не в dev-режиме.
Данные в серверном компоненте
Серверный компонент может быть асинхронным. Запрос к API или базе выполняется прямо в теле функции, без useEffect и промежуточного состояния.
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 },
});
if (!res.ok) throw new Error('products: ' + res.status);
return res.json();
}
export default async function Page() {
const products = await getProducts();
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
Обратите внимание на обработку ошибки: fetch не бросает исключение на 404 или 500. Без проверки res.ok компонент попытается разобрать HTML как JSON и упадёт с невнятным сообщением.
Динамические маршруты
В свежих версиях Next.js params и searchParams асинхронные. Их нужно дождаться, иначе TypeScript укажет на несоответствие типов, а рантайм выдаст предупреждение.
export default async function ProductPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const product = await getProduct(slug);
if (!product) notFound();
return <h1>{product.name}</h1>;
}
Если страница должна собираться заранее, добавьте generateStaticParams. Для каталога на тысячи позиций лучше не генерировать всё, а отдавать по требованию с кэшем. Параметры фильтра читают из searchParams и не кэшируют страницу целиком.
Кэш и ревалидация
Здесь чаще всего возникают сюрпризы. fetch с revalidate кэшируется на указанное время. cache: 'no-store' отключает кэш. Функции revalidatePath и revalidateTag сбрасывают кэш точечно после изменения данных.
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updateProduct(formData: FormData) {
await saveProduct(formData);
revalidateTag('products');
revalidatePath('/catalog');
}
Тег задаётся при запросе: next: { tags: ['products'] }. Тогда после правки товара обновляется только связанный кэш, а не весь сайт. Это дешевле и предсказуемее, чем глобальный сброс.
Метаданные и SEO
Заголовок и описание генерируются на сервере через generateMetadata. Функция может делать запросы, но они дублируют запрос страницы, поэтому кэш и cache() из React здесь особенно полезны.
export async function generateMetadata({ params }) {
const { slug } = await params;
const product = await getProduct(slug);
return {
title: product.name,
description: product.summary,
alternates: { canonical: '/product/' + slug },
};
}
Для соцсетей добавьте Open Graph. Если контент персонализирован, канонический URL и метаданные не должны зависеть от пользователя, иначе поисковик увидит разные версии одной страницы.
Потоковая отдача
Тяжёлый блок не обязан задерживать весь ответ. Оберните его в Suspense, и сервер отправит оболочку сразу, а данные догрузит позже. Файл loading.tsx делает то же на уровне сегмента.
import { Suspense } from 'react';
export default function Page() {
return (
<main>
<h1>Каталог</h1>
<Suspense fallback={<p>Загрузка...</p>}>
<ProductList />
</Suspense>
</main>
);
}
Не оборачивайте в Suspense всё подряд: каждый блок — отдельный чанк и лишние перерисовки. Выносите то, что действительно медленное. Если ошибка возникает внутри такой границы, её ловит ближайший error.tsx.
Клиентские компоненты
Директива 'use client' ставится в начало файла. Такой компонент получает пропсы от серверного родителя, но не может напрямую читать базу. Для интерактивности это нормально, для секретов — нет: всё, что попало в клиентский бандл, считается публичным.
Классическая ошибка — обращение к window или localStorage при рендере. На сервере этих объектов нет, и сборка падает. Решение — useEffect или проверка typeof window !== 'undefined'. Токены API держите только в серверных компонентах и маршрутных обработчиках.
Продакшен и Docker
Для контейнера удобен режим standalone: он собирает минимальный сервер без лишних зависимостей. Включается в next.config.js, после чего в образ копируют .next/standalone и .next/static.
module.exports = { output: 'standalone' };
npm run build
docker build -t shop:latest .
docker run -p 3000:3000 --env-file .env.production shop:latest
Переменные окружения читаются на сервере. Публичные значения должны начинаться с NEXT_PUBLIC_, и менять их после сборки бесполезно — они вшиваются в бандл.
Диагностика
Если страница не обновляется, проверьте, не закэширована ли она на сборке. Если падает 500, смотрите серверные логи. Если возникает рассинхрон разметки, ищите данные, которые зависят от времени или случайности.
- страница статическая, хотя зависит от пользователя;
- кэш не сброшен после изменения данных;
- клиентский компонент повторно запрашивает API при монтировании.
curl -sI https://shop.example.com/catalog | grep -i cache
docker logs --tail 100 shop
npm run build 2>&1 | tail -30
npx next info
Заголовок Cache-Control в ответе показывает, кто и насколько закэшировал страницу. Это первое, что стоит проверить, когда правки «не доезжают» до пользователей.
Итог
SSR в Next.js — это выбор режима для каждого маршрута, а не глобальный переключатель. Держите данные на сервере, отмечайте клиентские компоненты осознанно, настраивайте теги ревалидации и проверяйте поведение на build, а не только в dev. Тогда страницы будут быстрыми, а кэш перестанет быть источником загадочных багов.