В классической веб-разработке время жизни кэша определялось единственным скалярным числом — TTL (например, заголовок max-age=3600 или директива export const revalidate = 60). Однако в высоконагруженных распределенных системах единый TTL неизбежно приводит к двум фундаментальным проблемам:
Next.js 16 решает эти проблемы архитектурным разделением управления кэшем на две специализированные функции:
cacheLife() — заменяет скалярный TTL на трёхточечный временной профиль: stale, revalidate и expire.cacheTag() — связывает кэшируемую функцию или поддерево React Server Components с семантическими сущностями предметной области для точечной инвалидации по требованию.Вместо единственного таймера cacheLife() оперирует тремя последовательными временными барьерами:
ВРЕМЕННАЯ ШКАЛА ЖИЗНЕННОГО ЦИКЛА ЗАПИСИ КЭША:
0с stale revalidate expire ∞
├─────────────────────────┼──────────────────────┼─────────────────────────┼─────────────────►
│ ЗОНА 1: │ ЗОНА 2: │ ЗОНА 3: │ ЗОНА 4: │
│ Абсолютная свежесть │ Клиентский SWR │ Серверный SWR │ Hard Expire │
│ (Fresh Hit на клиенте │ (Клиент запрашивает │ (Сервер отдает stale, │ (Кэш уничтожен, │
│ и сервере) │ сервер, сервер Hit) │ но запускает revalidate)│ блокирующий I/O)│
└─────────────────────────┴──────────────────────┴─────────────────────────┴─────────────────┘stale (Клиентское окно свежести):
Время, в течение которого клиентский маршрутизатор (Client Router Cache) считает полученные данные абсолютно свежими. При переходе по ссылкам внутри приложения браузер вообще не отправляет сетевой запрос к серверу, мгновенно отображая интерфейс из оперативной памяти.revalidate (Окно фонового обновления Stale-While-Revalidate):
Период, по истечении которого сервер при входящем запросе немедленно возвращает клиенту имеющуюся (устаревшую) копию данных, но асинхронно в фоновом воркере запускает вычисление свежего значения и обновляет хранилище кэша. Клиент не блокируется сетевой задержкой.expire (Критическое истечение / Hard Expire):
Предельный срок жизни записи. Если запрос поступил после временной метки expire, рантайм признает кэш непригодным для отдачи. Запрос блокируется (Blocking I/O), ожидая завершения выполнения функции или рендеринга компонента.Next.js предоставляет набор готовых профилей, оптимизированных под типичные шаблоны нагрузки:
| Профиль | stale (Клиент) | revalidate (Сервер) | expire (Хранилище) | Типичный сценарий |
| :--- | :--- | :--- | :--- | :--- |
| 'seconds' | 0 сек | 1 сек | 60 сек | Биржевые котировки, вспышечные распродажи |
| 'minutes' | 5 мин | 1 мин | 1 час | Каталоги товаров, новостные сводки |
| 'hours' | 5 мин | 1 час | 1 день | Статьи базы знаний, карточки компаний |
| 'days' | 5 мин | 1 день | 1 неделя | Профили авторов, статические обзоры |
| 'weeks' | 5 мин | 1 неделя | 1 месяц | Политики конфиденциальности, архивные отчеты |
| 'max' | 5 мин | 1 месяц | 1 год | Справочники стран, неизменяемые конфигурации |
Кэшируемые серверные компоненты могут образовывать глубокие иерархии. Если родительский компонент вызывает дочерние кэшируемые функции или рендерит вложенные кэшируемые компоненты:
cacheTag() во вложенных функциях, автоматически всплывают и привязываются к родительской записи кэша. Инвалидация любого дочернего тега автоматически сбрасывает и весь родительский компонент.'days', а вызванная внутри него вспомогательная функция применила cacheLife('minutes'), итоговое эффективное время жизни родительского кэша схлопывается до 'minutes'.КАСКАДНОЕ СХЛОПЫВАНИЕ В ДЕРЕВЕ СЕРВЕРНЫХ КОМПОНЕНТОВ:
┌────────────────────────────────────────────────────────┐
│ [Page Showcase] cacheLife('days'), tag: 'showcase' │
│ │ │
│ ├──► [ProductCard] cacheLife('hours'), tag: 'p-101' │
│ │ │
│ └──► [FlashBadge] cacheLife('seconds'), tag: 'sale' │
└────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ ИТОГОВАЯ ЗАПИСЬ КЭША SHOWCASE: │
│ - Эффективный cacheLife: 'seconds' (наименьший!) │
│ - Агрегированные теги: ['showcase', 'p-101', 'sale'] │
└────────────────────────────────────────────────────────┘Разработчик объявляет внутри 'use cache' функции вызовы cacheLife('minutes') и cacheTag('catalog', `item:${id}`).
Рантайм собирает все дочерние теги в общий Set и вычисляет минимум среди всех интервалов stale, revalidate и expire.
Запись помещается в распределенное хранилище. Теги связываются с ключом записи через обратные индексы в Redis.
При вызове revalidateTag('catalog') рантайм мгновенно находит все связанные записи в дереве и переводит их в состояние expired.
Запись кэша имеет параметры: stale: 60с, revalidate: 300с, expire: 3600с. Прошло 400 секунд с момента генерации кэша. Пользователь открывает страницу. Каково поведение рантайма?
В карточке товара название и технические характеристики обновляются редко (профиль 'days'), тогда как остаток на складе и текущая цена со скидкой могут меняться ежесекундно. Изоляция виджета цены с cacheLife('seconds') и тегом stock:${sku} позволяет не сбрасывать кэш всего описания при каждом изменении складских запасов.
Дашборд финансовых показателей оперирует миллионами транзакций. Кастомный профиль analyticsHeavy с revalidate: 600 (10 минут) и expire: 86400 (24 часа) гарантирует, что ночные тяжелые отчеты никогда не приведут к отказу базы данных при пике утренней активности финансистов.
Статья блога связывается сразу с несколькими тегами: post:${slug}, author:${authorId}, category:${catId}. Редактор меняет биографию автора — вызывается инвалидация тега author:${authorId}, что мгновенно обновляет только статьи данного автора, сохраняя в кэше миллионы остальных материалов.
В архитектуре SaaS каждому магазину присваивается тег тенанта: tenant:${tenantId}. При обновлении глобальных настроек брендинга или локализации одного клиента инвалидируется единственный тег арендатора, не затрагивая соседние компании на том же кластере Kubernetes.
next.config.ts#В Next.js 16 кастомные профили объявляются в секции experimental.cacheLife:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
dynamicIO: true,
cacheLife: {
// Кастомный профиль для финансовых котировок
tickerFeed: {
stale: 10, // 10 секунд клиентской свежести
revalidate: 30, // 30 секунд серверного SWR
expire: 120, // 2 минуты до жесткого удаления
},
// Профиль для тяжелых агрегированных отчетов
heavyReporting: {
stale: 300, // 5 минут на клиенте
revalidate: 1800,// 30 минут фоновой актуальности
expire: 86400, // 24 часа предельного хранения
},
},
},
};
export default nextConfig;cacheLife и cacheTag в асинхронных функциях данных#import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
import { db } from '@/lib/db';
export interface ProductDetails {
id: string;
title: string;
price: number;
tags: string[];
}
export async function getProductById(productId: string): Promise<ProductDetails | null> {
'use cache';
// Назначаем стандартный профиль 'hours' (1 час revalidate)
cacheLife('hours');
// Привязываем функцию к семантическим тегам сущности
cacheTag('products', `product:${productId}`);
const product = await db.product.findUnique({
where: { id: productId },
select: { id: true, title: true, price: true, categoryId: true },
});
if (!product) return null;
// Дополнительно привязываем тег категории
cacheTag(`category:${product.categoryId}`);
return {
id: product.id,
title: product.title,
price: product.price,
tags: [product.categoryId],
};
}import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
import { getProductById } from '@/lib/products';
import { InventoryStatus } from '@/components/InventoryStatus';
interface Props {
productId: string;
}
export async function ProductPageSection({ productId }: Props) {
'use cache';
// Родитель запрашивает длительное время жизни 'days'
cacheLife('days');
cacheTag(`section:product:${productId}`);
const product = await getProductById(productId);
if (!product) {
return <div className="text-text-muted">Товар не найден</div>;
}
return (
<section className="border border-border rounded-2xl p-6 bg-bg-surface">
<h1 className="text-2xl font-bold text-text-primary">{product.title}</h1>
<p className="text-accent-cyan font-mono text-xl mt-2">${product.price}</p>
{/* Вложенный компонент с более частым обновлением и собственными тегами */}
<InventoryStatus productId={productId} />
</section>
);
}Рантайм начинает рендеринг поддерева. Компонент InventoryStatus вызывает cacheLife('seconds') и регистрирует тег 'stock:101'.
Дочерние теги сохраняются в контексте текущего стрима.
Родительский компонент собирает массив тегов ['section:product:101', 'products', 'product:101', 'stock:101'].
Инвалидация любого из этих 4 тегов инвалидирует родителя.
Сравнение cacheLife('days') родителя и cacheLife('seconds') потомка. Эффективный expire схлопывается до 60 секунд.
Защита от отдачи устаревших дочерних данных в теле родителя.
Сериализованный Flight Payload сохраняется в кэш с полученным минимумом TTL и полным набором агрегированных тегов.
В Redis создаются обратные индексы SADD для каждого тега.
Главная страница каталога использует cacheLife('days'). В футере страницы разработчик отрендерил компонент LiveCurrencyRate, внутри которого вызван cacheLife('seconds'). Как это повлияет на кэширование всей главной страницы?
Включение вспомогательного серверного компонента с коротким временем жизни (например, плашки текущего времени или статуса онлайна с cacheLife('seconds')) внутрь тяжелого долгоживущего родительского компонента с cacheLife('weeks') приводит к тому, что вся тяжелая страница начинает пересчитываться каждую минуту.
Решение: Выносите сверхдинамичные блоки за пределы родительской границы 'use cache' либо изолируйте их границей <Suspense>, чтобы динамика стримилась отдельно без деградации кэша статической оболочки.
Генерация миллионов уникальных тегов на каждый запрос (например, включение временных меток Date.now(), случайных UUID или несортированных параметров поиска: cacheTag('search:' + JSON.stringify(query))). Это приводит к исчерпанию оперативной памяти в Redis и деградации производительности поиска по обратным индексам тегов.
Решение: Ограничивайте теги стабильными сущностями предметной области (entity:id, type:category).
Вызов этих функций внутри обычного серверного компонента или вспомогательной утилиты без объявленной директивы 'use cache' приведет к ошибке во время исполнения или будет полностью проигнорирован рантаймом.
Решение: cacheLife() и cacheTag() являются контекстно-зависимыми API, функционирующими исключительно внутри лексической области директивы 'use cache'.