Перейти к содержимому

8. Server-Side Rendering (SSR)

SSR — это когда HTML генерируется на сервере при каждом запросе. Пользователь получает свежий, полностью заполненный HTML прямо с сервера. Не пустую страницу, не устаревший кеш — именно свежий контент для этого конкретного запроса! 🌐

Запрос пользователя
Next.js Server
├─► cookies() / headers() ─── данные запроса
├─► searchParams ─── параметры URL
├─► DB запрос ─── свежие данные
Рендеринг HTML
Ответ клиенту
(готовый HTML с данными!)

🔄 Динамический рендеринг: Как включается SSR

Заголовок раздела «🔄 Динамический рендеринг: Как включается SSR»

В Next.js App Router страница становится динамической (SSR) когда:

// 1. Используется cookies() — каждый запрос имеет свои cookies
import { cookies } from 'next/headers';
export default async function ProfilePage() {
const cookieStore = await cookies(); // ← Делает страницу динамической!
const sessionToken = cookieStore.get('session');
const user = await getUserBySession(sessionToken?.value);
return <UserProfile user={user} />;
}
// 2. Используется headers()
import { headers } from 'next/headers';
export default async function LocalizedPage() {
const headersList = await headers(); // ← Динамическая!
const locale = headersList.get('accept-language') ?? 'ru';
const content = await getContentByLocale(locale);
return <Page content={content} />;
}
// 3. Используется searchParams (параметры URL ?q=...)
interface Props {
searchParams: Promise<{ q?: string; page?: string }>;
}
export default async function SearchPage({ searchParams }: Props) {
const { q = '', page = '1' } = await searchParams; // ← Динамическая!
const results = await searchPosts(q, parseInt(page));
return (
<div>
<SearchInput defaultValue={q} />
<ResultsList results={results} />
<Pagination currentPage={parseInt(page)} />
</div>
);
}
// 4. Явная настройка force-dynamic
export const dynamic = 'force-dynamic'; // ← Явно динамическая!
export const revalidate = 0; // Альтернатива: revalidate = 0
export default async function AlwaysFreshPage() {
const data = await getLatestData();
return <DataDisplay data={data} />;
}
// 5. noStore() — отключить кеш для конкретного запроса
import { unstable_noStore as noStore } from 'next/cache';
export default async function RealtimePage() {
noStore(); // ← Делает текущий рендер динамическим
const price = await getCurrentPrice();
return <PriceDisplay price={price} />;
}

Streaming — это отправка HTML по частям, пока медленные компоненты ещё загружаются:

app/dashboard/page.tsx
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
{/* Мгновенно: статический контент */}
<h1>Дашборд</h1>
<QuickStats /> {/* Быстро — нет Suspense */}
{/* Streaming: отправляется когда готово */}
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart /> {/* Медленный запрос */}
</Suspense>
<Suspense fallback={<TransactionsSkeleton />}>
<TransactionsTable /> {/* Ещё один медленный */}
</Suspense>
</div>
);
}
// Медленный компонент — не блокирует остальное!
async function RevenueChart() {
await new Promise(r => setTimeout(r, 2000)); // Симуляция медленного API
const revenue = await getMonthlyRevenue();
return <Chart data={revenue} />;
}
async function TransactionsTable() {
const transactions = await getRecentTransactions();
return <Table data={transactions} />;
}
Без Streaming:
────────────────────────────────── 3s
| ждём всё... |→ Страница появляется целиком
С Streaming:
|====| 0.3s → Базовый HTML + скелетоны
|======| 1s → RevenueChart встраивается
|========| 2s → TransactionsTable встраивается

HTTP Response:
──────────────────────────────────────
Content-Type: text/html
Transfer-Encoding: chunked
<-- Chunk 1 (мгновенно): -->
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<h1>Дашборд</h1>
<div id="__NEXT_SUSPENSE__0">
<!-- skeleton placeholder -->
<div class="skeleton">Loading chart...</div>
</div>
<-- Chunk 2 (через 1s, когда RevenueChart готов): -->
<script>
// Вставляем готовый HTML в placeholder
$RC("__NEXT_SUSPENSE__0", `<div class="chart">...</div>`)
</script>
<-- Chunk 3 (через 2s): -->
<script>$RC("__NEXT_SUSPENSE__1", `<table>...</table>`)</script>
</body>
</html>
──────────────────────────────────────

⚙️ Конфигурация динамического рендеринга

Заголовок раздела «⚙️ Конфигурация динамического рендеринга»
// Варианты export const dynamic:
export const dynamic = 'auto'; // По умолчанию — Next.js решает
export const dynamic = 'force-dynamic'; // Всегда SSR
export const dynamic = 'error'; // Ошибка если нужен динамический рендер
export const dynamic = 'force-static'; // Всегда статика (игнор cookies и т.д.)
// Варианты export const revalidate:
export const revalidate = 0; // Как force-dynamic
export const revalidate = false; // Как force-cache (никогда не обновлять)
export const revalidate = 3600; // ISR — обновлять каждый час
// runtime: Edge vs Node.js
export const runtime = 'edge'; // Edge Functions (Cloudflare Workers-like)
export const runtime = 'nodejs'; // Node.js (по умолчанию)
// fetchCache
export const fetchCache = 'force-no-store'; // Все fetch() без кеша

// SSR — используй когда:
// ✅ Данные должны быть СВЕЖИМИ при каждом запросе
// ✅ Контент персонализирован (зависит от пользователя/сессии)
// ✅ Используешь cookies/headers
// ✅ Поисковые результаты (searchParams)
// Примеры SSR:
// - Лента новостей (всегда свежие)
// - Профиль пользователя (персонализировано)
// - Корзина покупок
// - Страница поиска
// - Dashboard с реальными данными
// SSG — используй когда:
// ✅ Контент редко меняется
// ✅ Одинаков для всех пользователей
// ✅ Нужна максимальная скорость
// Примеры: блоги, документация, лендинги
// ISR — используй когда:
// ✅ Контент меняется редко, но не статичен
// ✅ Нужна скорость + относительная свежесть
// Примеры: каталог товаров, новости
// CSR — используй когда:
// ✅ Только за логином (SEO не нужен)
// ✅ Очень интерактивная (дашборд, редактор)
// ✅ Данные меняются в реальном времени

Типичный паттерн — проверка авторизации в SSR:

app/dashboard/page.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { verifyToken } from '@/lib/jwt';
export default async function DashboardPage() {
const cookieStore = await cookies();
const token = cookieStore.get('auth-token');
if (!token) {
redirect('/login'); // Серверный редирект — нет мигания!
}
const payload = await verifyToken(token.value);
if (!payload) {
redirect('/login?error=invalid-token');
}
const user = await db.user.findUnique({
where: { id: payload.userId },
include: { profile: true },
});
if (!user) {
redirect('/login');
}
return (
<div>
<h1>Привет, {user.name}!</h1>
<DashboardContent user={user} />
</div>
);
}
// Вся логика авторизации — на сервере! Клиент никогда не видит неавторизованный контент.

Next.js 14+ предлагает экспериментальную фичу — Partial Prerendering:

next.config.mjs
const nextConfig = {
experimental: {
ppr: 'incremental', // или true для всего приложения
},
};
// app/product/[id]/page.tsx
export const experimental_ppr = true;
import { Suspense } from 'react';
export default function ProductPage({ params }) {
return (
<div>
{/* СТАТИЧЕСКАЯ часть — предрендерится при сборке */}
<ProductBreadcrumbs />
<StaticProductInfo />
{/* ДИНАМИЧЕСКАЯ часть — потокова при запросе */}
<Suspense fallback={<PriceSkeleton />}>
<DynamicPrice productId={params.id} /> {/* Реальная цена */}
</Suspense>
<Suspense fallback={<InventorySkeleton />}>
<InventoryStatus productId={params.id} /> {/* Наличие */}
</Suspense>
</div>
);
}
// Статическая оболочка + динамические "дырки" = лучшее из обоих миров!

  1. Динамический рендеринг включается автоматически при: cookies(), headers(), searchParams, noStore()
  2. force-dynamic — явный способ включить SSR
  3. Streaming — HTML отправляется по частям через <Suspense>
  4. Edge Runtime — выполнение на CDN-узлах (быстрее, но меньше APIs)
  5. PPR — комбинация статики и динамики в одной странице (future!)