Почему кэширование в Next.js 16 изменилось кардинально
Если вы работали с предыдущими версиями Next.js, то наверняка сталкивались с одной неприятной особенностью: кэширование было неявным. Фреймворк автоматически кэшировал fetch-запросы и статические страницы, а разработчикам приходилось отключать это через cache: 'no-store' или dynamic = 'force-dynamic'. Честно говоря, это порождало кучу путаницы — данные кэшировались когда не нужно, а ревалидация работала как-то... непредсказуемо.
Next.js 16 перевернул эту модель с ног на голову. Теперь весь код динамический по умолчанию, а кэширование стало явным и декларативным через новую директиву "use cache". Каждый маршрут, компонент или функция выполняются в реальном времени, пока вы явно не скажете: «Эй, этот результат можно кэшировать».
Итак, давайте разберём всё по порядку — от базового применения use cache до продвинутых стратегий ревалидации с cacheTag и updateTag.
Что такое директива «use cache»
Директива "use cache" — это новый примитив языкового уровня в Next.js 16. Работает она по тому же принципу, что и "use client" или "use server": добавляете строку в начало функции или файла, и компилятор понимает — результат выполнения можно кэшировать.
И вот что действительно круто: в отличие от устаревшего unstable_cache, который требовал ручного управления ключами и обёртывания функций, use cache интегрирован прямо в компилятор. Он анализирует аргументы функции, замыкания и даже props компонентов, чтобы автоматически генерировать ключ кэша. Никаких магических строк.
Где можно применять «use cache»
Директива работает на трёх уровнях:
- Уровень функции — кэширование результата асинхронной функции (например, запроса к базе данных)
- Уровень компонента — кэширование отрендеренного вывода серверного компонента
- Уровень страницы/лейаута — кэширование всего маршрута целиком
Включение Cache Components в проекте
Прежде чем начать использовать use cache, нужно включить Cache Components в конфигурации:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
После этого становятся доступны все три варианта директивы и связанные API: cacheLife, cacheTag, updateTag.
Минимальные требования: Node.js 20.9.0 и TypeScript 5.1.0. Если используете более старые версии — время обновиться.
Три варианта директивы «use cache»
Next.js 16 предоставляет три варианта кэширования. Каждый — для своего сценария.
1. «use cache» — кэширование в памяти (по умолчанию)
Стандартная директива хранит данные в оперативной памяти сервера с использованием LRU-алгоритма. Для большинства задач — это именно то, что нужно.
async function getProducts() {
'use cache'
const products = await db.product.findMany()
return products
}
Есть нюанс: кэш теряется при перезапуске сервера и может вытесняться при нехватке памяти. И данные не разделяются между экземплярами — так что для одиночного инстанса это идеально, а для кластера стоит посмотреть дальше.
2. «use cache: remote» — удалённый кэш
Если у вас несколько экземпляров сервера в продакшене (а в 2026 году это скорее норма), понадобится общее хранилище — Redis, KV или что-то подобное:
async function getGlobalConfig() {
'use cache: remote'
const config = await fetch('https://api.example.com/config')
return config.json()
}
Результат сохраняется во внешнем хранилище и доступен всем инстансам. Снижает нагрузку на источники данных, но добавляет сетевые задержки — тут нужно искать баланс.
3. «use cache: private» — клиентский кэш
Это экспериментальная штука. Она позволяет обращаться к runtime API (cookies(), headers(), searchParams) внутри кэшированного контекста:
async function getUserPreferences() {
'use cache: private'
const cookieStore = await cookies()
const theme = cookieStore.get('theme')?.value ?? 'light'
return { theme }
}
Важный момент: результаты use cache: private кэшируются только в памяти браузера. На сервере ничего не сохраняется. Перезагрузил страницу — кэша нет.
Управление временем жизни кэша с cacheLife
Функция cacheLife из next/cache задаёт политику кэширования. У неё три параметра, и каждый отвечает за свой аспект:
- stale — как долго клиент использует кэш без проверки сервера
- revalidate — после истечения этого времени следующий запрос запустит фоновое обновление
- expire — после этого времени без запросов кэш удаляется полностью
Если это напоминает вам стратегию stale-while-revalidate из HTTP-кэширования — вы абсолютно правы, логика та же.
Встроенные профили
Для типичных сценариев есть готовые профили, чтобы не задавать числа вручную каждый раз:
import { cacheLife } from 'next/cache'
async function getStaticContent() {
'use cache'
cacheLife('days') // Для редко меняющегося контента
return await fetchContent()
}
async function getFrequentData() {
'use cache'
cacheLife('hours') // Для данных средней частоты обновлений
return await fetchData()
}
async function getLiveData() {
'use cache'
cacheLife('minutes') // Для часто меняющихся данных
return await fetchLiveData()
}
Пользовательские профили
Когда встроенных профилей недостаточно, можно определить свои в конфигурации. На практике это встречается довольно часто:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
catalog: {
stale: 3600, // 1 час — клиент использует кэш
revalidate: 900, // 15 минут — фоновое обновление
expire: 86400, // 24 часа — полное удаление
},
userSession: {
stale: 300, // 5 минут
revalidate: 60, // 1 минута
expire: 3600, // 1 час
},
},
}
export default nextConfig
А дальше используете профиль по имени — просто и читаемо:
async function getProductCatalog() {
'use cache'
cacheLife('catalog')
return await db.product.findMany({ where: { isActive: true } })
}
Условное кэширование
Вот паттерн, который я считаю особенно элегантным — разная длительность кэша в зависимости от результата:
import { cacheLife, cacheTag } from 'next/cache'
async function getArticle(slug: string) {
'use cache'
cacheTag(`article-${slug}`)
const article = await db.article.findUnique({ where: { slug } })
if (!article) {
cacheLife('minutes') // Ненайденные статьи — кэшируем ненадолго
return null
}
cacheLife('days') // Опубликованный контент — кэшируем надолго
return article
}
Смысл в том, что страницу 404 мы не хотим кэшировать на сутки — вдруг статью опубликуют через пять минут. А уже существующий контент менять будут нечасто.
Инвалидация кэша: cacheTag, revalidateTag и updateTag
Временное кэширование через cacheLife — это хорошо, но часто нужно сбрасывать кэш по событию. Скажем, редактор обновил статью — и пользователи должны видеть свежую версию.
Пометка кэша тегами
Функция cacheTag привязывает кэш-запись к одному или нескольким тегам:
import { cacheTag, cacheLife } from 'next/cache'
async function getUserProfile(userId: string) {
'use cache'
cacheLife('hours')
cacheTag('users', `user-${userId}`)
return await db.user.findUnique({ where: { id: userId } })
}
revalidateTag — фоновое обновление
revalidateTag помечает все кэш-записи с указанным тегом как устаревшие. При следующем запросе сервер отдаёт старые данные (stale-while-revalidate), а в фоне обновляет кэш:
'use server'
import { revalidateTag } from 'next/cache'
export async function updateUserRole(userId: string, role: string) {
await db.user.update({ where: { id: userId }, data: { role } })
revalidateTag(`user-${userId}`, 'max')
}
Обратите внимание: начиная с Next.js 16.2+, одноаргументная форма revalidateTag(tag) устарела. Теперь нужно использовать двухаргументную: revalidateTag(tag, 'max').
updateTag — мгновенная инвалидация
Бывают случаи, когда пользователь должен увидеть изменения сразу — паттерн «read your own writes». Для этого есть updateTag:
'use server'
import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
const post = await db.post.create({
data: { title, content },
})
updateTag('posts') // Немедленно сбрасывает кэш
redirect(`/posts/${post.id}`) // Пользователь видит свежие данные
}
Ключевое отличие от revalidateTag: updateTag немедленно удаляет кэш, без stale-while-revalidate. Но есть ограничение — updateTag доступен только в Server Actions.
Миграция с unstable_cache на «use cache»
Если ваш проект ещё использует unstable_cache, пора переезжать. Но учтите — это не простая замена одного вызова на другой. Это переход на новую архитектуру.
До: unstable_cache (Next.js 15)
import { unstable_cache } from 'next/cache'
export const getCachedUser = (userId: string) => {
return unstable_cache(
async () => {
return db.user.findUnique({ where: { id: userId } })
},
[`user-${userId}`], // Ручные ключи кэша
{ tags: [`user-${userId}`], revalidate: 3600 }
)()
}
После: «use cache» (Next.js 16)
import { cacheLife, cacheTag } from 'next/cache'
async function getUser(userId: string) {
'use cache'
cacheLife('hours')
cacheTag(`user-${userId}`)
return db.user.findUnique({ where: { id: userId } })
}
Разница видна невооружённым глазом:
- Нет обёртки — функция выглядит как обычный async-вызов
- Ключи кэша генерируются автоматически на основе аргументов
- Теги и время жизни задаются декларативно, прямо внутри функции
use cacheможет кэшировать не только JSON, но и компоненты, и целые маршруты
Пошаговый план миграции
- Включите
cacheComponents: trueвnext.config.ts - Найдите все вызовы
unstable_cacheв проекте (обычный grep справится) - Для каждого вызова: извлеките функцию из обёртки, добавьте
"use cache"в начало - Замените числовой
revalidateна профильcacheLife - Замените массив
tagsна вызовыcacheTag - Обновите все
revalidateTagна двухаргументную форму - Проверьте, что
cookies()иheaders()не вызываются внутриuse cache— вынесите их наружу
По моему опыту, миграция среднего проекта занимает пару часов. Основная сложность — не в синтаксисе, а в пересмотре стратегии кэширования.
Интеграция с Partial Prerendering (PPR)
Директива use cache тесно связана с Partial Prerendering — и это, пожалуй, самая интересная часть. PPR позволяет объединить статический и динамический контент на одной странице.
Как это работает
При сборке Next.js создаёт статическую оболочку из кэшированных частей страницы. Динамические части оборачиваются в <Suspense> и стримятся клиенту при запросе. Вот как это выглядит на практике:
import { Suspense } from 'react'
import { cacheLife, cacheTag } from 'next/cache'
// Эта часть попадёт в статическую оболочку
async function ProductCatalog() {
'use cache'
cacheLife('hours')
cacheTag('catalog')
const products = await db.product.findMany({
where: { isActive: true },
orderBy: { createdAt: 'desc' },
})
return (
Каталог товаров
{products.map(p => (
- {p.name} — {p.price} ₽
))}
)
}
// Динамическая часть — стримится при запросе
async function UserCart() {
const cookieStore = await cookies()
const cartId = cookieStore.get('cartId')?.value
const cart = await db.cart.findUnique({ where: { id: cartId } })
return
}
// Страница объединяет оба подхода
export default function ShopPage() {
return (
}>
)
}
Результат впечатляет: пользователь мгновенно видит каталог из кэша, а корзина подгружается параллельно. TTFB снижается на 60–80% по сравнению с полностью динамическими страницами. На реальных проектах это ощутимая разница.
Типичные ошибки и подводные камни
При работе с use cache легко наступить на грабли. Вот самые частые — чтобы вы не наступали.
1. Вызов runtime API внутри «use cache»
Нельзя вызывать cookies(), headers() или обращаться к searchParams внутри функции с "use cache". Эти API возвращают данные, уникальные для каждого запроса, и кэшировать их бессмысленно (и опасно).
// ❌ Неправильно
async function getDashboard() {
'use cache'
const cookieStore = await cookies() // Ошибка!
const userId = cookieStore.get('userId')?.value
return await fetchDashboard(userId)
}
// ✅ Правильно
async function getDashboardData(userId: string) {
'use cache'
cacheTag(`dashboard-${userId}`)
return await fetchDashboard(userId)
}
// В компоненте — извлекаем userId до кэшированного вызова
export default async function DashboardPage() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
const data = await getDashboardData(userId!)
return
}
2. Слишком широкое кэширование
Не размещайте use cache на уровне лейаута, если дочерние компоненты зависят от динамических данных. Кэшируйте ближе к источнику данных:
// ❌ Слишком широко — кэширует весь лейаут
export default async function Layout({ children }) {
'use cache'
return {children}
}
// ✅ Точечно — кэшируем только данные навигации
async function getNavItems() {
'use cache'
cacheLife('days')
return await db.navItem.findMany({ orderBy: { sortOrder: 'asc' } })
}
3. Кэш с очень коротким временем жизни
Кэш с revalidate менее 5 минут или нулевым expire автоматически исключается из пререндеринга и становится динамической «дырой». Это by design, но может застать врасплох, если вы рассчитывали на статическую оболочку.
4. Несовместимость с Edge Runtime
Директива use cache работает только в среде Node.js. Для Edge Runtime используйте fetch() с опцией next.revalidate. Это ограничение архитектурное, и вряд ли оно изменится в ближайших релизах.
Лучшие практики кэширования в Next.js 16
Несколько рекомендаций, основанных на реальном опыте работы с новой системой:
- Кэшируйте данные, а не UI — размещайте
use cacheна функциях доступа к данным, а не на компонентах (если нет веской причины) - Используйте теги для связанных данных — группируйте кэш-записи тегами (
'products','users'), чтобы инвалидировать их одним вызовом - Предпочитайте cacheTag + revalidateTag вместо
cacheLifeдля контента из CMS — не ждите таймера, сбрасывайте кэш при публикации - Используйте updateTag в Server Actions для операций, где пользователь ждёт мгновенного результата
- Определяйте пользовательские профили в
next.config.tsдля единообразия во всём проекте - Тестируйте в dev-режиме — Next.js 16 показывает предупреждения, если динамические данные используются вне
<Suspense>
Часто задаваемые вопросы
Чем «use cache» отличается от unstable_cache?
unstable_cache — это библиотечная функция с ручным управлением ключами. "use cache" — директива компилятора: добавляете строку, а Next.js сам генерирует ключи на основе аргументов и замыканий. Плюс use cache умеет кэшировать компоненты и маршруты, а не только JSON.
Можно ли использовать «use cache» с динамическими маршрутами?
Да, без проблем. Аргументы функции (включая параметры маршрута) автоматически становятся частью ключа кэша. Каждая комбинация параметров — отдельная кэш-запись. Так что getProduct(productId) создаст свой кэш для каждого productId.
Как работает кэширование при деплое на Vercel?
На Vercel "use cache" использует встроенное in-memory LRU-хранилище по умолчанию. Для "use cache: remote" Vercel предоставляет интегрированный KV-хранилище через конфигурацию cacheHandlers. Это позволяет разделять кэш между серверлесс-функциями и edge-нодами.
Что произойдёт, если кэшированная функция выбросит ошибку?
Ошибки не кэшируются — и это правильное решение. Если функция внутри use cache бросает исключение, оно пробрасывается вызывающему коду, а в кэш ничего не записывается. Следующий запрос повторит вызов. Для предсказуемых ошибок лучше возвращать объект результата вместо throw.
Поддерживает ли «use cache» работу с Prisma и Drizzle ORM?
Да, use cache совместим с любым ORM. Кэшируется сериализуемый результат функции, поэтому неважно, используете вы Prisma, Drizzle, Kysely или прямые SQL-запросы. Единственное условие — возвращаемые данные должны быть сериализуемы (без функций, классов или циклических ссылок).