У високонавантажених веб-застосунках затримка бази даних та зовнішніх мікросервісів безпосередньо визначає TTFB та користувацький досвід. Неконтрольоване виконання повторюваних SQL-запитів та HTTP-викликів перевантажує обчислювальні потужності сервера та призводить до вичерпання пулів з'єднань.
В архітектурі Next.js App Router реалізована багаторівнева система кешування, що об'єднує можливості ядра React та рантайму Next.js. Вона охоплює весь життєвий цикл даних — від дедуплікації викликів в оперативній пам'яті одного серверного рендеру до розподіленого зберігання статичних сторінок на CDN та кешування сегментів маршрутів у браузері клієнта.
Глибоке розуміння кожного рівня кешу, правил його деоптимізації та механізмів он-деманд інвалідації відрізняє розробника, який бореться із застарілими даними в продакшені, від архітектора, що проєктує консистентні та швидкі розподілені системи.
У Next.js функціонують 4 незалежні рівні кешування, кожен з яких вирішує вузькоспеціалізовану задачу, має ізольовану область видимості та власний життєвий цикл:
Нижче представлено інтерактивний симулятор потоку кешування. Перемикайте сценарії, щоб вивчити проходження запиту крізь усі 4 рівні:
Досліджуйте проходження запитів через Router Cache, Full Route Cache, Memoization та Data Cache
GET /catalog HTTP/1.1 (Без локального кешу)Користувач вперше відкриває сторінку. У браузері немає кешу маршрутів. Запит надходить на сервер, де Full Route Cache та Data Cache перевіряють статичні дані.
Під час переходу за посиланням браузер перевіряє кеш сегментів в оперативній пам'яті вкладки.
У Next.js 15+ для динамічних роутів staleTime дорівнює 0s (запит відразу надсилається на сервер). Для статичних — 5 хвилин.
Сервер перевіряє наявність скомпільованого статичного HTML та бінарного RSC Flight Payload.
При збігу (Hit) клієнту миттєво віддається готова відповідь із CDN/сервера без виконання серверного коду компонентів.
Якщо маршрут динамічний, React починає рендеринг дерева Server Components. Ідентичні fetch() дедуплікуються.
Кожен унікальний fetch перевіряє наявність збереженої відповіді на диску сервера або в зовнішньому CacheHandler.
Якщо TTL не закінчився — повертаються збережені дані. Якщо час минув, віддається stale-кеш і у фоні запускається ревалідація.
При промаху кешу (miss) запит надходить до реальної БД або зовнішнього API. Відповідь зберігається в Data Cache.
React автоматично дедуплікує виклики fetch з однаковим URL та однаковим набором опцій у межах одного дерева рендерингу серверних компонентів. Життєвий цикл цього кешу обмежений мілісекундами генерації відповіді.
[!IMPORTANT] Request Memoization не використовується спільно між користувачами та сесіями. Це оптимізація всередині окремого запиту єдиного дерева рендеру React Server Components. Для неї немає глобального прапорця вимкнення, проте її можна обійти, передавши унікальний
AbortController(через властивістьsignal) уfetch.
// app/components/Header.tsx
export async function Header() {
// Виконується реальний мережевий запит
const user = await fetch('https://api.internal/user/me').then(r => r.json());
return <header>Користувач: {user.name}</header>;
}
// app/components/Sidebar.tsx
export async function Sidebar() {
// Дані миттєво витягуються з Request Memoization (RAM) без повторного HTTP-запиту
const user = await fetch('https://api.internal/user/me').then(r => r.json());
return <aside>Роль: {user.role}</aside>;
}У версіях Next.js 15+ стандартний fetch за замовчуванням є некешованим (cache: 'no-store'). Кешування на рівні Data Cache вмикається явно.
// 1. За замовчуванням у Next.js 15+: no-store (завжди свіжі дані з мережі)
const freshData = await fetch('https://api.internal/rates');
// 2. Персистентне кешування (зберігається до явної ревалідації)
const immutableData = await fetch('https://api.internal/dictionary', {
cache: 'force-cache',
});
// 3. Time-based revalidation (Stale-While-Revalidate на 60 секунд)
const catalog = await fetch('https://api.internal/products', {
next: { revalidate: 60 },
});
// 4. Тегування запитів для точкової On-demand інвалідації
const profile = await fetch('https://api.internal/user/10', {
next: { tags: ['user-10', 'users'] },
});Для скидання застарілих даних за подією (Server Action, обробка Webhook) використовуються функції з модуля next/cache. Вони одночасно інвалідують записи в Data Cache та залежні сегменти у Full Route Cache.
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
import { products } from '@/lib/schema';
import { eq } from 'drizzle-orm';
export async function updateProductPrice(productId: string, newPrice: number): Promise<{ success: boolean }> {
// 1. Мутація даних в основній базі даних
await db.update(products).set({ price: newPrice }).where(eq(products.id, productId));
// 2. Точкове скидання кешу за тегом (зачіпає всі запити з цим тегом у всьому застосунку)
revalidateTag(`product-${productId}`);
revalidateTag('catalog-list');
// 3. Інвалідація конкретного маршруту
revalidatePath(`/products/${productId}`, 'page');
return { success: true };
}Клієнт викликає Server Action, всередині якого виконується мутація в БД та викликається revalidateTag('catalog'). Яким чином браузер отримує оновлений інтерфейс?
При встановленні опції next: { revalidate: 60 } система працює за чітким протоколом:
Прямі запити до баз даних (Prisma, Drizzle, pg) не використовують глобальний fetch, тому не потрапляють у Data Cache автоматично. Для них застосовується unstable_cache, або директива 'use cache' (Next.js 15.1+ з прапорцем dynamicIO).
import 'server-only';
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
import { projects } from '@/lib/schema';
import { eq } from 'drizzle-orm';
export interface ProjectDTO {
id: string;
name: string;
stars: number;
}
export const getCachedProject = (projectId: string) => {
return unstable_cache(
async (id: string): Promise<ProjectDTO | null> => {
const record = await db.query.projects.findFirst({
where: eq(projects.id, id),
});
return record ?? null;
},
['project-detail-key', projectId], // Ключі кешу
{
revalidate: 3600, // 1 година
tags: [`project-${projectId}`, 'projects'],
}
)(projectId);
};Маршрут автоматично перемикається зі статичного режиму (Static Generation) у динамічний (Dynamic Rendering при кожному запиті), якщо в його коді викликаються динамічні функції:
import { cookies, headers } from 'next/headers';
// Примусове вимкнення Full Route Cache на рівні всього сегмента
export const dynamic = 'force-dynamic';
export default async function AnalyticsPage() {
// Асинхронне читання заголовків та cookies у Next.js 15+ робить рендер строго динамічним
const headersList = await headers();
const userAgent = headersList.get('user-agent') ?? 'unknown';
const cookieStore = await cookies();
const sessionId = cookieStore.get('sid')?.value;
return (
<main className="p-6">
<h1>Системна аналітика</h1>
<p>Клієнт: {userAgent}</p>
<p>Сесія: {sessionId}</p>
</main>
);
}Застосунок Next.js розгорнутий у 4 подах (репліках) за балансувальником Nginx. Адміністратор змінює ціну товару, викликаючи revalidateTag('products'). Чому частина користувачів продовжує бачити стару ціну?
За замовчуванням Next.js зберігає Data Cache та Full Route Cache у локальній файловій системі контейнера за шляхом .next/cache/.
Якщо ваш застосунок розгорнутий у кластері з кількох подів (реплік) за балансувальником навантаження, виклик revalidateTag('products') на репліці №1 очистить кеш тільки у файловій системі репліки №1. Репліки №2 та №3 продовжуватимуть віддавати застарілі дані, викликаючи мерехтіння інтерфейсу для користувачів.
Рішення: Для продакшен-середовищ із горизонтальним масштабуванням обов'язково налаштуйте кастомний CacheHandler (на базі Redis або Key-Value сховища) і підключіть його в next.config.ts через директиву cacheHandler.
Розробники нерідко використовують прапорець 'layout' для швидкого скидання всього розділу: revalidatePath('/dashboard', 'layout').
Це призводить до миттєвого скидання кешу всіх дочірніх сторінок, паралельних слотів та вкладених сегментів усередині цього макета. Якщо в розділі сотні сторінок, перший же наплив користувачів викличе ефект «Thundering Herd» (шторм запитів до БД), повністю нівелюючи переваги ISR.
Рішення: Завжди віддавайте перевагу гранулярній інвалідації за тегами (revalidateTag), або скидайте конкретні сторінки за замовчуванням без прапорця 'layout'.
Функція unstable_cache ізолює обчислення від контексту поточного HTTP-запиту. Якщо всередині закешованого колбека спробувати прочитати cookies(), headers() або звертатися до несеріалізовних об'єктів, Next.js викине помилку DynamicServerError.
Рішення: Передавайте динамічні дані у вигляді явних аргументів, що входять до ключа кешу.
// ❌ ПОМИЛКА: Спроба читання cookies всередині кешованої функції
const getAuthData = unstable_cache(
async () => {
const cookieStore = await cookies(); // Викличе виняток DynamicServerError!
return fetchUserData(cookieStore.get('token')?.value);
},
['auth-user-key']
);
// ✅ ПРАВИЛЬНО: Передавайте динамічні дані у вигляді явних аргументів, що входять до ключа кешу
const getAuthData = (token: string) =>
unstable_cache(
async (authToken: string) => fetchUserData(authToken),
['auth-user-key', token]
)(token);При використанні unstable_cache для викликів БД (не fetch), легко помилитися в ключах кешу: якщо аргументи функції не включені в масив ключів, усі користувачі отримають дані першого запитувача.
Рішення: Завжди включайте динамічні параметри (наприклад ID запису) до складеного ключа кешу.
// ❌ Помилка: Ключ кешу статичний, тому всі клієнти побачать дані першого запитаного ID
const getUser = unstable_cache(
async (id: string) => db.query.users.findFirst(eq(users.id, id)),
['user']
);
// ✅ Правильно: ID включений до складеного ключа кешу
const getUser = unstable_cache(
async (id: string) => db.query.users.findFirst(eq(users.id, id)),
['user', id]
);