В высоконагруженных веб-приложениях латентность базы данных и внешних микросервисов напрямую определяет 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) запрос уходит в реальную БД или внешний REST 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 } система работает по строгому протоколу:
В Next.js 16 на смену устаревшему unstable_cache пришла официальная парадигма Cache Components на базе директивы 'use cache'. Директива 'use cache' может применяться как к целым серверным компонентам, так и к отдельным асинхронным функциям уровня Data Access Layer (DAL).
Управление временем жизни и тегами кэша осуществляется декларативными функциями cacheLife() и cacheTag() из модуля next/cache:
import 'server-only';
import { cacheLife, cacheTag } 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;
}
// 1. Современный стандарт Next.js 16: 'use cache' на уровне функции
export async function getProjectById(projectId: string): Promise<ProjectDTO | null> {
'use cache';
cacheLife('hours'); // Пресет времени жизни: seconds, minutes, hours, days, max
cacheTag(`project-${projectId}`, 'projects');
const record = await db.query.projects.findFirst({
where: eq(projects.id, projectId),
});
return record ?? null;
}
// 2. Использование 'use cache' напрямую на уровне Server Component
export async function ProjectWidget({ projectId }: { projectId: string }) {
'use cache';
cacheLife('days');
cacheTag(`project-${projectId}`);
const project = await getProjectById(projectId);
if (!project) return null;
return (
<div className="p-4 rounded-xl border border-border">
<h3 className="font-bold">{project.name}</h3>
<span>Звезд: {project.stars}</span>
</div>
);
}Для обратной совместимости со старыми кодовыми базами Next.js 14/15 функция unstable_cache по-прежнему поддерживается, но для всех новых модулей 2026 года стандартом является директива 'use cache'.
Маршрут автоматически переключается из статического режима (Static Generation) в динамический (Dynamic Rendering при каждом запросе), если в его коде вызываются динамические API:
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]
);