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. Тогда страницы будут быстрыми, а кэш перестанет быть источником загадочных багов.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте