У сучасних вебдодатках різні сторінки мають принципово відмінні вимоги щодо швидкості віддачі, актуальності контенту та навантаження на інфраструктуру:
Next.js App Router об'єднує ці сценарії в єдину гібридну систему рендерингу. Замість застарілого розділення на рівні сторінок (getServerSideProps / getStaticProps з Pages Router), у сучасній архітектурі стратегія рендерингу визначається декларативно на рівні маршрутів (Route Segments) та окремих викликів даних (Data Cache).
У Next.js App Router існує три базові стратегії рендерингу:
cookies(), headers(), searchParams) або примусово через export const dynamic = 'force-dynamic'.Клієнт A Edge CDN Фоновий Воркер (Node.js)
│ │ │
(1) ├──── GET /product/42 ───────►│ │
│ │ [Кеш свіжий: age < 60s] │
│◄─── 200 OK (HTML/RSC) ──────┤ │
│ │ │
═══════╪═════════════════════════════╪═════════════════════════════════╪═══════
│ (Минуло 65 секунд: кеш став STALE) │
│ │ │
(2) │ Клієнт B │ │
├──── GET /product/42 ───────►│ │
│ │ [Кеш застарів! Віддаємо stale] │
│◄─── 200 OK (STALE копія) ───┤ │
│ │ │
│ ├── Асинхронний тригер рендеру ──►│
│ │ ├── Запит до БД
│ │ ├── Генерація HTML/RSC
│ │◄── Новий HTML/RSC Payload ──────┤
│ │ [Кеш оновлено: age = 0s] │
│ │ │
(3) │ Клієнт C │ │
├──── GET /product/42 ───────►│ │
│ │ [Кеш свіжий!] │
│◄─── 200 OK (СВІЖІ дані) ────┤ │Сторінка рендериться під час збірки (SSG) або за вимогою першого користувача. Згенерований HTML та RSC Flight зберігаються у Full Route Cache та Edge CDN.
Протягом заданого часу (наприклад, revalidate = 60) усі користувачі миттєво отримують відповідь із кешу CDN із наднизьким TTFB.
Після 60 секунд кеш позначається як Stale (застарілий). При наступному запиті CDN миттєво віддає клієнту стару копію без очікування сервера.
Next.js паралельно ініціює ізольований фоновий процес рендерингу сторінки зі зверненням до джерел даних.
Після успішної збірки новий HTML та RSC Payload атомарно перезаписують кеш. Усі наступні запити отримують оновлений контент.
Що відбудеться з користувачем, який запросив сторінку з терміном revalidate = 60 секунд через 65 секунд після її останньої генерації?
Поведінку сторінки можна явно задати за допомогою експорту спеціальних констант із page.tsx або layout.tsx:
// 1. Примусовий Dynamic SSR (жодного кешування сторінки)
export const dynamic = 'force-dynamic';
// 2. Примусовий SSG (динамічні функції повернуть порожні значення)
// export const dynamic = 'force-static';
// 3. Time-based ISR: ревалідувати сторінку не частіше одного разу на 60 секунд
// export const revalidate = 60;
// 4. Поведінка для параметрів, не згенерованих у generateStaticParams:
// true: генерувати динамічно при першому запиті та зберігати в ISR (за замовчуванням)
// false: повертати 404
export const dynamicParams = true;
export default async function DashboardPage() {
return <div>Динамічна панель управління</div>;
}fetch() у Next.js 15+#[!IMPORTANT] Архітектурний стандарт: Починаючи з Next.js 15, глобальний виклик
fetch()за замовчуванням НЕ кешується (cache: 'no-store'). Це прямо протилежно поведінці Next.js 13/14!
Для керування кешем окремих мережевих запитів передавайте параметри в об'єкті конфігурації fetch:
// 1. SSG: Безстрокове кешування (Permanent Data Cache)
const staticData = await fetch('https://api.example.com/config', {
cache: 'force-cache'
});
// 2. Time-based ISR: Кешування з інвалідацією за часом (1 година)
const timedData = await fetch('https://api.example.com/products', {
next: {
revalidate: 3600,
tags: ['products-list'] // Тег для скидання за вимогою
}
});
// 3. SSR: Запит без кешування (поведінка Next.js 15+ за замовчуванням)
const liveData = await fetch('https://api.example.com/notifications', {
cache: 'no-store'
});revalidateTag та revalidatePath#Скидання кешу за подією (наприклад, при збереженні товару в адмінці або за вхідним вебхуком із CMS) виконується на сервері через Server Actions або Route Handlers:
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
interface UpdateProductDTO {
title: string;
price: number;
}
export async function updateProduct(id: string, data: UpdateProductDTO): Promise<{ success: boolean }> {
// 1. Мутація даних у БД
await db.product.update({
where: { id },
data
});
// 2. Інвалідація конкретного кеш-тегу (точково скидає пов'язані запити)
revalidateTag(`product-${id}`);
// 3. Інвалідація маршруту каталогу
revalidatePath('/catalog', 'page');
return { success: true };
}unstable_cache#Якщо дані запитуються через ORM (Prisma, Drizzle, TypeORM), виклик fetch відсутній. Щоб підключити такі запити до системи ISR та Data Cache, використовуйте утиліту unstable_cache:
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
// Огортаємо функцію доступу до БД у кешуючий шар Next.js
export const getCachedPost = unstable_cache(
async (slug: string) => {
return await db.post.findUnique({
where: { slug }
});
},
['post-by-slug'], // Ключові частини кешу (Key parts)
{
revalidate: 300, // ISR: 5 хвилин
tags: ['posts'] // Тег для On-demand скидання через revalidateTag('posts')
}
);В інтернет-магазині з 200 000 товарів попередньо збирати всі сторінки недоцільно. Патерн об'єднує generateStaticParams для топ-100 товарів та автоматичний фоновий ISR для всіх інших:
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
interface PageProps {
params: Promise<{ id: string }>; // У Next.js 15+ доступ суворо асинхронний
}
// 1. Попередня збірка топ-100 популярних сторінок при білді
export async function generateStaticParams() {
const popularProducts = await db.product.findMany({
where: { isPopular: true },
take: 100,
select: { id: true }
});
return popularProducts.map((p) => ({ id: p.id }));
}
// 2. Дозволяємо генерацію решти 199 900 товарів на льоту при першому візиті
export const dynamicParams = true;
// 3. Оновлювати сторінку у фоні не частіше ніж раз на 10 хвилин
export const revalidate = 600;
export default async function ProductPage({ params }: PageProps) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
if (!product) {
notFound();
}
return (
<article className="p-8 max-w-3xl mx-auto">
<h1 className="text-3xl font-bold">{product.title}</h1>
<p className="text-2xl font-mono text-accent-cyan mt-2">{product.price} ₴</p>
<div className="mt-4 text-text-muted leading-relaxed">{product.description}</div>
</article>
);
}import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
// Захист ендпоінта секретним токеном
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ message: 'Невірний токен' }, { status: 401 });
}
const body = (await request.json()) as { tag?: string };
if (!body.tag) {
return NextResponse.json({ message: 'Тег не передано' }, { status: 400 });
}
// Миттєве скидання Data Cache для всіх запитів із цим тегом
revalidateTag(body.tag);
return NextResponse.json({ revalidated: true, tag: body.tag, now: Date.now() });
}Яка команда найефективніше оновить лише блок рекомендованих товарів без перезбірки всієї HTML-сторінки каталогу?
У Next.js 13 та 14 будь-які виклики fetch() автоматично мемоїзувалися та кешувалися зі стратегією force-cache. Розробники звикли, що сторінки зі статичними fetch генеруються в SSG.
У Next.js 15+ семантика змінилася на протилежну: fetch() за замовчуванням виконується з опцією cache: 'no-store'. Якщо ви напишете await fetch('https://api.com/items') без явних налаштувань кешу, Next.js зробить сторінку динамічною (SSR), і вона заново запитуватиме API при кожному візиті користувача!
Рішення: Завжди явно оголошуйте стратегію кешування (force-cache для SSG або next: { revalidate: 60 } для ISR).
// Для SSG:
const res = await fetch(url, { cache: 'force-cache' });
// Для ISR:
const res = await fetch(url, { next: { revalidate: 60 } });Якщо в будь-якому (навіть глибоко вкладеному) серверному компоненті викликати динамічну функцію await cookies() або await headers(), Next.js змушений перервати статичну збірку та перевести весь маршрут у режим Dynamic SSR.
Рішення:
<Suspense fallback={<CartSkeleton />}> — у цьому випадку каркас сторінки залишиться в SSG/ISR, а динамічний слот дострімиться клієнту.// components/CartBadge.tsx (Server Component у сайдбарі)
import { cookies } from 'next/headers';
export async function CartBadge() {
const cookieStore = await cookies(); // ❌ Ламає SSG/ISR для всього сайту без Suspense!
const cartId = cookieStore.get('cartId')?.value;
return <span>{cartId ? 'Товарів: 1' : 'Кошик порожній'}</span>;
}Коли закінчується таймер revalidate: 60, перша вхідна сесія ініціює фонову перегенерацію сторінки. Якщо серверна функція рендерингу виконується 3-5 секунд, а на сайт надходить 500 RPS, без розподілених блокувань може виникнути ситуація, коли десятки фонових воркерів паралельно навантажують базу даних важкими SQL-запитами.
Рішення на рівні архітектури: У Next.js вбудовано механізм дедуплікації регенерації (Stale-While-Revalidate lock), проте для прямих запитів до БД через ORM обов'язково використовуйте unstable_cache з тегами та стратегією явної On-demand ревалідації (revalidateTag) за вебхуком замість агресивного time-based полінгу.
Виклик revalidatePath('/') очищає кеш тільки для головної сторінки. Щоб скинути кеш для всього сайту цілком, необхідно передати тип 'layout': revalidatePath('/', 'layout'). Проте зловживання цією конструкцією в продакшені призводить до різких піків навантаження на БД, оскільки тисячі сторінок інвалідуються одночасно. Надавайте перевагу точковому revalidateTag().