Пошук уроків, статей та іншого контенту
Налаштуєте кешування сторінок, даних і API в production та контролюватимете інвалідацію кешу.
У production Next.js може кешувати різні рівні застосунку:
Data Cache — результати fetch або інших серверних запитів до даних.
Full Route Cache — згенерований результат статичного маршруту: HTML і RSC payload.
Router Cache — кеш сегментів маршруту на стороні браузера.
Кеш CDN або reverse proxy — кешування HTTP-відповідей перед Next.js.
Ці рівні пов’язані, але не є одним кешем. Наприклад, інвалідація Data Cache може спричинити повторну генерацію сторінки, але не обов’язково миттєво очистить уже відкриту сторінку в браузері.
У production не варто покладатися на неявні значення кешування. Явно задавайте:
час життя кешу;
теги для групової інвалідації;
маршрути, які потрібно перебудувати;
спосіб оновлення даних після мутації.
fetchДля серверного fetch можна задати час повторної валідації:
const response = await fetch("https://api.example.com/products", {
next: {
revalidate: 300,
},
});
const products = await response.json();У цьому прикладі результат зберігається в Data Cache і може використовуватися протягом 300 секунд. Після цього Next.js вважатиме запис застарілим і виконає новий запит.
Для інвалідації конкретної групи даних використовуйте теги:
const response = await fetch("https://api.example.com/products", {
next: {
revalidate: 300,
tags: ["products"],
},
});Тег не є ключем конкретного запису. Це мітка, за якою можна одночасно інвалідувати всі пов’язані запити.
Наприклад, список товарів і сторінка окремого товару можуть мати спільний тег:
await fetch("https://api.example.com/products", {
next: {
revalidate: 300,
tags: ["products"],
},
});
await fetch(`https://api.example.com/products/${id}`, {
next: {
revalidate: 300,
tags: ["products", `product:${id}`],
},
});Тоді:
тег products інвалідує весь кеш товарів;
тег product:42 інвалідує лише товар із ідентифікатором 42.
fetch автоматично підтримує Data Cache, але запити до ORM або драйвера бази даних потрібно кешувати окремо. Для цього можна використати unstable_cache.
import { unstable_cache } from "next/cache";
import { db } from "@/lib/db";
export const getProducts = unstable_cache(
async () => {
return db.product.findMany({
orderBy: {
createdAt: "desc",
},
});
},
["products-list"],
{
revalidate: 300,
tags: ["products"],
},
);Функція getProducts повертатиме кешований результат. Після зміни товарів кеш потрібно інвалідувати за тегом products.
Ключ кешу має бути стабільним. Якщо результат залежить від аргументів, вони повинні враховуватися у ключі або передаватися в кешовану функцію так, щоб Next.js міг розрізняти результати.
Для даних, що залежать від користувача, не використовуйте спільний кеш без урахування ідентифікатора користувача. Інакше один користувач може отримати дані іншого.
Сторінка може потрапити до Full Route Cache, якщо Next.js здатен згенерувати її статично.
Для маршруту можна задати інтервал ISR:
// app/catalog/page.tsx
export const revalidate = 300;
import { getProducts } from "@/lib/products";
export default async function CatalogPage() {
const products = await getProducts();
return (
<main>
<h1>Каталог</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name}
</li>
))}
</ul>
</main>
);
}У production сторінка може бути згенерована під час build або першого запиту, а потім обслуговуватися з кешу. Після завершення інтервалу Next.js використовує механізм повторної валідації.
export const revalidate впливає на маршрут загалом. Якщо одному з джерел даних потрібен коротший інтервал, задайте next.revalidate безпосередньо для відповідного fetch.
Якщо сторінка використовує кешовані дані, вона може бути статичною. Коли ці дані інвалідуються, Next.js має повторно згенерувати залежний результат маршруту.
Наприклад:
// app/catalog/page.tsx
export const revalidate = 600;
export default async function CatalogPage() {
const response = await fetch("https://api.example.com/products", {
next: {
revalidate: 600,
tags: ["products"],
},
});
const products = await response.json();
return (
<main>
<h1>Каталог</h1>
{products.map((product: { id: number; name: string }) => (
<article key={product.id}>{product.name}</article>
))}
</main>
);
}Якщо викликати revalidateTag("products", "max"), кеш даних стане недійсним. Під час наступного запиту сторінка отримає актуальні дані й буде згенерована повторно.
Route Handler не слід вважати кешованим за замовчуванням. Особливо в сучасних версіях Next.js GET-маршрути потрібно кешувати явно.
// app/api/products/route.ts
import { NextResponse } from "next/server";
export const revalidate = 300;
export async function GET() {
const response = await fetch("https://api.example.com/products", {
next: {
revalidate: 300,
tags: ["products"],
},
});
if (!response.ok) {
return NextResponse.json(
{ error: "Не вдалося отримати товари" },
{ status: 502 },
);
}
const products = await response.json();
return NextResponse.json(products, {
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=60",
},
});
}Тут є два різні рівні:
next.revalidate кешує запит Next.js до бекенда;
Cache-Control повідомляє CDN або reverse proxy, як кешувати HTTP-відповідь.
Ці налаштування не замінюють одне одного. Якщо API використовується тільки серверними компонентами, достатньо Data Cache. Якщо API викликається браузером або зовнішніми клієнтами, важливий також HTTP-кеш.
Не кешуйте відповідь, якщо вона залежить від cookie, заголовка авторизації або конкретного користувача:
// app/api/me/route.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET() {
const session = (await cookies()).get("session");
if (!session) {
return NextResponse.json(
{ error: "Неавторизований запит" },
{ status: 401 },
);
}
return NextResponse.json(
{ userId: session.value },
{
headers: {
"Cache-Control": "private, no-store",
},
},
);
}Для персональних відповідей використовуйте:
dynamic = "force-dynamic" або еквівалентну динамічну конфігурацію;
Cache-Control: private, no-store;
відсутність спільних тегів і публічного CDN-кешу.
Інвалідація означає повідомлення Next.js, що кешований результат більше не можна вважати актуальним.
revalidateTagrevalidateTag інвалідує дані, пов’язані з тегом:
import { revalidateTag } from "next/cache";
revalidateTag("products", "max");Використовуйте теги для доменних сутностей:
products;
product:42;
orders:user:17;
settings:global.
Теги краще зашивати в одному місці, щоб уникнути помилок у рядках:
// lib/cache-tags.ts
export const cacheTags = {
products: "products",
product: (id: number) => `product:${id}`,
};revalidatePathrevalidatePath інвалідує конкретний маршрут:
import { revalidatePath } from "next/cache";
revalidatePath("/catalog");
revalidatePath("/catalog/42");Використовуйте його, коли потрібно перебудувати певну сторінку після зміни даних.
Теги відповідають на запитання «які дані змінилися?», а шляхи — «які сторінки потрібно перебудувати?». Часто після мутації потрібні обидва виклики.
Окремий захищений Route Handler може отримувати webhook від CMS або бекенда:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const secret = request.headers.get("x-revalidate-secret");
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json(
{ error: "Недійсний секрет" },
{ status: 401 },
);
}
const body = (await request.json()) as {
type?: string;
id?: number;
};
if (body.type === "product" && body.id) {
revalidateTag("products", "max");
revalidateTag(`product:${body.id}`, "max");
revalidatePath("/catalog");
revalidatePath(`/catalog/${body.id}`);
}
return NextResponse.json({ revalidated: true });
}Такий endpoint обов’язково потрібно захистити. Не створюйте публічний маршрут, який дозволяє будь-кому очищати кеш.
Після створення або оновлення даних інвалідуйте кеш у тому самому серверному сценарії, де виконується мутація:
// app/actions/products.ts
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
import { db } from "@/lib/db";
export async function updateProduct(
id: number,
name: string,
) {
await db.product.update({
where: { id },
data: { name },
});
revalidateTag("products", "max");
revalidateTag(`product:${id}`, "max");
revalidatePath("/catalog");
revalidatePath(`/catalog/${id}`);
}Порядок важливий:
спочатку успішно змінити дані;
лише потім інвалідувати кеш;
якщо мутація завершилася помилкою, не очищати кеш.
Нижче наведено мінімальний приклад для каталогу:
// app/catalog/page.tsx
type Product = {
id: number;
title: string;
price: number;
};
export const revalidate = 300;
async function getProducts(): Promise<Product[]> {
const response = await fetch("https://dummyjson.com/products", {
next: {
revalidate: 300,
tags: ["products"],
},
});
if (!response.ok) {
throw new Error("Не вдалося завантажити каталог");
}
const data = (await response.json()) as {
products: Product[];
};
return data.products;
}
export default async function CatalogPage() {
const products = await getProducts();
return (
<main>
<h1>Каталог товарів</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.title}: {product.price} $
</li>
))}
</ul>
</main>
);
}API для примусової інвалідації:
// app/api/revalidate-products/route.ts
import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
if (
request.headers.get("x-revalidate-secret") !==
process.env.REVALIDATE_SECRET
) {
return NextResponse.json(
{ error: "Недійсний секрет" },
{ status: 401 },
);
}
revalidateTag("products", "max");
revalidatePath("/catalog");
return NextResponse.json({
revalidated: true,
timestamp: new Date().toISOString(),
});
}Запит для інвалідації:
curl -X POST http://localhost:3000/api/revalidate-products \
-H "x-revalidate-secret: your-secret"Після цього:
запит до https://dummyjson.com/products більше не використовуватиме старе значення Data Cache;
сторінка /catalog буде перебудована під час наступного запиту;
CDN-кеш, якщо він налаштований окремо, може вимагати власного purge або коротшого s-maxage.
| Тип даних | Рекомендована стратегія | |---|---| | Публічний каталог | revalidate і тег | | Рідко змінюваний контент | Великий інтервал ISR | | Дані після webhook | Інвалідація через revalidateTag | | Профіль користувача | Динамічний маршрут і private, no-store | | Результат пошуку | Зазвичай динамічний запит | | Публічний API | Явний HTTP-кеш через Cache-Control |
На одному сервері кеш може зберігатися локально. У кластері з кількома інстансами виникають додаткові вимоги:
усі інстанси повинні бачити узгоджений кеш або отримувати однакові інвалідації;
локальна інвалідація одного інстансу не повинна залишати застарілі дані на інших;
CDN-кеш і кеш Next.js потрібно розглядати окремо;
після деплою не можна припускати, що всі старі записи автоматично зникли.
Для багатосерверного production-оточення використовуйте спільне сховище кешу або механізм синхронізації інвалідацій, передбачений вашою інфраструктурою. Конкретна реалізація залежить від платформи розгортання.
Для перевірки кешування контролюйте:
час відповіді першого і повторного запиту;
кількість звернень до бази даних;
виконання webhook інвалідації;
помилки під час генерації сторінок;
значення заголовків CDN;
випадки, коли персональна відповідь потрапила до публічного кешу.
Не визначайте стан кешу лише за локальним середовищем. Production CDN, кілька інстансів і серверна платформа можуть мати власні правила.
Для товару можна застосувати такий алгоритм:
Сторінка каталогу читає дані з тегом products.
Сторінка окремого товару читає дані з тегами products і product:id.
Адміністратор змінює товар.
Сервер успішно зберігає зміни.
Викликаються revalidateTag для списку і товару.
Викликаються revalidatePath для каталогу і сторінки товару.
Наступні запити отримують актуальні дані.
Відкритий браузер може вимагати навігації або оновлення, якщо старий результат уже зберігається в Router Cache.
Поведінка кешу відрізнялася між версіями Next.js. Явно задавайте revalidate, no-store, теги й HTTP-заголовки замість припущень.
Не додавайте дані користувача до публічного кешу:
await fetch("/api/profile", {
next: {
revalidate: 300,
},
});Якщо відповідь залежить від сесії, такий підхід може бути небезпечним. Для персональних даних використовуйте динамічне отримання і no-store.
revalidatePath("/catalog") не замінює інвалідацію даних, якщо сторінка знову прочитає старий запис із Data Cache. Для повного оновлення інвалідуйте відповідний тег.
Зворотна ситуація також можлива: дані стануть актуальними, але вже згенерований результат маршруту або зовнішній CDN ще віддаватиме стару відповідь. Для критичних сторінок перевіряйте і тег, і шлях, і HTTP-кеш.
Якщо спочатку викликати revalidateTag, а потім операція запису до бази завершиться помилкою, кеш буде очищено без зміни даних. Інвалідуйте кеш лише після успішної мутації.
Секрет інвалідації не можна передавати в URL або залишати без перевірки. Використовуйте секретний заголовок, перевірку підпису webhook і обмеження доступу на рівні інфраструктури.
Інвалідація серверного кешу не завжди змінює вже відрендерений інтерфейс у браузері. Після мутації потрібно також оновити дані на клієнті або виконати навігацію, якщо це передбачено UX застосунку.
Next.js має окремі рівні кешування: Data Cache, Full Route Cache, Router Cache і HTTP/CDN-кеш.
Для production задавайте кешування явно.
next.revalidate визначає час повторної валідації серверних даних.
next.tags дозволяє групувати пов’язані записи.
revalidateTag інвалідує дані за доменним тегом.
revalidatePath інвалідує конкретний маршрут.
Публічні API можна кешувати через Cache-Control, а персональні відповіді мають бути private, no-store.
Інвалідацію виконуйте після успішної мутації та враховуйте CDN і кілька production-інстансів.