Современная парадигма кэширования в Next.js 16: разделение ответственности через изолированный Data Access Layer (DAL), декларативная директива 'use cache', управление временем жизни через cacheLife(), гранулярный сброс по тегам через cacheTag() и revalidateTag().
В ранних версиях App Router (Next.js 13-14) кэширование по умолчанию вызывало серьезные сложности:
fetch() приводило к показу устаревших данных при мутациях и требовало постоянного использования { cache: 'no-store' }.revalidatePath() сбрасывал кэш слишком грубо, заставляя пересобирать всю страницу вместо одного обновленного виджета.В Next.js 16 и React 19 модель кэширования была фундаментально переработана:
'use cache'.server-only.cacheLife и cacheTag.Архитектура 4 уровней кэширования Next.js 16 представляет собой упорядоченный конвейер фильтрации данных:
+-------------------------------------------------------------------------------+
| УРОВЕНЬ 1: Request Memoization (React.cache) |
| - Время жизни: Длительность одного HTTP-запроса пользователя |
| - Назначение: Дедупликация идентичных SQL/fetch вызовов в разных RSC |
+-----------------------------------+-------------------------------------------+
| (miss)
+-----------------------------------v-------------------------------------------+
| УРОВЕНЬ 2: Data Cache ('use cache' / cacheTag) |
| - Время жизни: Персистентный кэш между запросами и пользователями |
| - Назначение: Хранение результатов DAL, профилирование cacheLife('hours') |
+-----------------------------------+-------------------------------------------+
| (miss)
+-----------------------------------v-------------------------------------------+
| УРОВЕНЬ 3: Full Route Cache / Partial Prerendering (PPR Shell) |
| - Время жизни: До новой сборки деплоя или программной ревалидации |
| - Назначение: Статический скомпилированный HTML/RSC Shell на Edge CDN |
+-----------------------------------+-------------------------------------------+
| (miss)
+-----------------------------------v-------------------------------------------+
| УРОВЕНЬ 4: Client-Side Router Cache |
| - Время жизни: Сессия пользователя в браузере (30-300 секунд) |
| - Назначение: Мгновенные переходы назад/вперед без обращения к серверу |
+-------------------------------------------------------------------------------+Создание директории src/dal с обязательным импортом пакета server-only для предотвращения случайного импорта серверного кода в клиентские бандлы.
Создание внутренней функции verifySession(), проверяющей JWT/куки до выполнения любых запросов к базе данных.
Маркировка публичных функций чтения директивой 'use cache' с назначением тегов через cacheTag('products', `product-${id}`).
Прямой вызов DAL-функций в серверных компонентах без посредников в виде HTTP Route Handlers.
Вызов revalidateTag('products') внутри серверных мутаций для мгновенного сброса кэша без даунтайма.
Что произойдет, если попытаться вызвать асинхронную функцию cookies() или headers() внутри функции, помеченной директивой 'use cache'?
Ниже представлена эталонная реализация корпоративного каталога: изоляция DAL, типизированный доступ к данным, профиль времени жизни cacheLife и мутация со сбросом тегов.
import 'server-only';
import { cacheTag } from 'next/dist/server/use-cache/cache-tag';
import { cacheLife } from 'next/dist/server/use-cache/cache-life';
export interface ProductDTO {
id: string;
title: string;
price: number;
inventory: number;
category: string;
}
// Имитация клиента базы данных (Prisma / Drizzle / pg)
const db = {
products: {
findMany: async (category?: string): Promise<ProductDTO[]> => {
// Имитация SQL-запроса с задержкой 120 ms
return [
{ id: 'prod-1', title: 'NVMe SSD Enterprise 4TB', price: 420, inventory: 15, category: 'storage' },
{ id: 'prod-2', title: 'ECC DDR5 RAM 64GB', price: 290, inventory: 40, category: 'memory' }
].filter(p => !category || p.category === category);
},
findById: async (id: string): Promise<ProductDTO | null> => {
if (id === 'prod-1') {
return { id: 'prod-1', title: 'NVMe SSD Enterprise 4TB', price: 420, inventory: 15, category: 'storage' };
}
return null;
}
}
};
/**
* Получение списка продуктов с глобальным кэшированием
*/
export async function getCachedProducts(category?: string): Promise<ProductDTO[]> {
'use cache';
// Устанавливаем профиль времени жизни: свежий 15 минут, stale 1 час
cacheLife('hours');
// Назначаем системные теги для точечной инвалидации
cacheTag('products', `products-cat-${category || 'all'}`);
return await db.products.findMany(category);
}
/**
* Получение детальной карточки продукта по ID
*/
export async function getCachedProductById(id: string): Promise<ProductDTO | null> {
'use cache';
cacheLife('days');
cacheTag('products', `product-id-${id}`);
return await db.products.findById(id);
}Теперь рассмотрим Server Action мутации, который обновляет цену товара и точечно инвалидирует только связанные теги без очистки всего кэша приложения:
'use server';
import { revalidateTag } from 'next/cache';
import * as z from 'zod';
const updateProductSchema = z.object({
productId: z.string().min(1),
newPrice: z.number().positive()
});
export type ActionState = {
success: boolean;
message?: string;
error?: string;
};
export async function updateProductPrice(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
try {
const rawData = {
productId: formData.get('productId'),
newPrice: Number(formData.get('price'))
};
const validated = updateProductSchema.safeParse(rawData);
if (!validated.success) {
return {
success: false,
error: validated.error.issues[0]?.message || 'Некорректные входные данные'
};
}
const { productId, newPrice } = validated.data;
// Выполнение UPDATE в базе данных
// await db.products.update({ where: { id: productId }, data: { price: newPrice } });
console.log(`[DB UPDATE] Товар ${productId} обновлен до $${newPrice}`);
// ТОЧЕЧНАЯ ИНВАЛИДАЦИЯ: сбрасываем только конкретный товар и список каталога
revalidateTag(`product-id-${productId}`);
revalidateTag('products');
return {
success: true,
message: 'Цена успешно обновлена, кэш синхронизирован!'
};
} catch (err: unknown) {
const errMsg = err instanceof Error ? err.message : 'Ошибка базы данных';
return {
success: false,
error: errMsg
};
}
}Если файл со слоем базы данных или приватными ключами API случайно будет импортирован клиентским компонентом (например, ради экспорта типа TypeScript), весь модуль или его части могут попасть в публичный JavaScript-бандл. Всегда добавляйте import 'server-only' в самую первую строку всех файлов слоя данных DAL.
Вызов revalidatePath('/') сбрасывает абсолютно все кэшированные страницы и компоненты сайта. Сервер вынужден заново выполнять тысячи тяжелых SQL-запросов под нагрузкой. Используйте семантические теги cacheTag('entity-name') и метод revalidateTag(), обновляющий только затронутые сущности.
Если поместить расчет персональной корзины покупателя или историю заказов под 'use cache', первый зашедший пользователь закэширует свои данные для всех остальных посетителей сайта. Персональные данные ДОЛЖНЫ быть некэшируемыми или кэшироваться исключительно на уровне React.cache() в пределах одного входящего запроса.
В чем ключевое архитектурное отличие между React.cache() и директивой 'use cache' в Next.js 16?