Якщо ви працювали з кешуванням у попередніх версіях Next.js App Router, то напевно знаєте, наскільки це могло бути... непередбачувано. Запити fetch() кешувалися автоматично, дані "застрягали" в кеші без очевидної причини, а розробники витрачали години на дебаг. Так ось, у Next.js 16 команда Vercel нарешті це виправила. Нова директива 'use cache' замінює всі попередні механізми й дає вам повний контроль над тим, що кешується, на скільки і коли оновлюється.
У цьому посібнику ми розберемо все — від базового налаштування до просунутих стратегій інвалідації. Будемо чесними: кешування ніколи не було простою темою, але новий API робить його значно зрозумілішим.
Що змінилося в кешуванні Next.js 16
Головна зміна проста, але радикальна: кешування тепер повністю опціональне (opt-in). За замовчуванням усі запити виконуються під час кожного запиту без жодного кешування. Хочете кешувати — скажіть про це явно.
Чесно кажучи, це саме той підхід, який мав бути з самого початку.
Ось ключові зміни:
- Прапорець
experimental.pprвидалено — замість нього використовуєтьсяcacheComponents - Нова директива
'use cache'працює на рівні сторінки, компонента або функції unstable_cacheвизнано застарілим — прощавай, "unstable" у назві API- Функції
cacheLifeтаcacheTagдають точний контроль над терміном життя та інвалідацією - Нова функція
updateTagзабезпечує миттєве оновлення кешу в Server Actions
Увімкнення Cache Components
Перш ніж щось кешувати, потрібно увімкнути функціональність у конфігурації проєкту. Це буквально два рядки:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
Після цього 'use cache' стає доступною в будь-якому серверному файлі. Бонус — ця опція автоматично активує Partial Prerendering (PPR), що дозволяє поєднувати статичний і динамічний контент на одному маршруті. Про це поговоримо трохи пізніше.
Три рівні застосування «use cache»
Директива 'use cache' працює на трьох рівнях: сторінки, компонента чи окремої функції. Кожен рівень створює незалежну точку кешування з автоматично згенерованими ключами. Давайте розберемо кожен.
Кешування на рівні сторінки
Найпростіший варіант — додати 'use cache' на початку файлу сторінки, і весь її вивід буде кешуватися:
// app/blog/page.tsx
'use cache'
import { cacheLife } from 'next/cache'
export default async function BlogPage() {
cacheLife('days')
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json())
return (
<main>
<h1>Блог</h1>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</main>
)
}
Компілятор автоматично генерує ключі кешу на основі маршруту та динамічних сегментів. Статична оболонка сторінки доставляється браузеру миттєво, динамічний контент підвантажується потоково. Все досить інтуїтивно.
Кешування на рівні компонента
А ось тут стає цікавіше. Коли на одній сторінці є і статичні елементи, і дані що швидко змінюються, кешувати всю сторінку цілком — не варіант. Натомість можна кешувати окремі компоненти:
// components/ProductCard.tsx
'use cache'
import { cacheLife, cacheTag } from 'next/cache'
interface ProductCardProps {
productId: string
}
export default async function ProductCard({ productId }: ProductCardProps) {
cacheLife('hours')
cacheTag(`product-${productId}`)
const product = await fetch(`https://api.example.com/products/${productId}`)
.then(res => res.json())
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>{product.description}</p>
<span className="price">${product.price}</span>
</div>
)
}
Тепер ProductCard кешується незалежно від сторінки, на якій відображається. Кожен продукт отримує власний тег — це стане в нагоді для точкової інвалідації (про це далі).
Кешування на рівні функції
Мій улюблений рівень, якщо чесно. Для кешування запитів до бази даних, зовнішніх API чи важких обчислень — просто додайте 'use cache' всередині функції:
// lib/data.ts
import { cacheTag, cacheLife } from 'next/cache'
export async function getCategories() {
'use cache'
cacheTag('categories')
cacheLife('weeks')
const categories = await db.query('SELECT * FROM categories ORDER BY name')
return categories
}
export async function getPostBySlug(slug: string) {
'use cache'
cacheTag(`post-${slug}`, 'posts')
cacheLife('days')
const post = await db.query('SELECT * FROM posts WHERE slug = ?', [slug])
return post
}
Зверніть увагу на важливу деталь: аргументи функції (наприклад, slug) автоматично стають частиною ключа кешу. Тобто різні значення slug — це різні записи в кеші. Не потрібно вручну формувати ключі, як раніше з unstable_cache.
Керування терміном життя кешу з cacheLife
Функція cacheLife визначає, як довго кешований контент залишається актуальним. Next.js 16 надає набір готових профілів, які покривають більшість сценаріїв:
'default'— базовий профіль, застосовується автоматично'seconds'— для даних, що постійно змінюються (курси валют, статуси)'minutes'— помірна частота оновлення'hours'— прайси, каталоги, лістинги'days'— блог-пости, статичний контент'weeks'— рідко змінюваний контент'max'— максимально тривале кешування
Кожен профіль контролює два параметри: stale (як довго клієнт використовує кешовані дані без перевірки) та revalidate (після якого часу наступний запит запустить фонове оновлення).
Але що робити, коли стандартних профілів не вистачає? Створіть власний:
import { cacheLife } from 'next/cache'
export async function getStockPrices() {
'use cache'
cacheLife({
stale: 30, // 30 секунд для клієнтського кешу
revalidate: 60, // ревалідація через 60 секунд
})
return await fetchStockData()
}
Для більшості випадків стандартних профілів цілком вистачає, але для фінансових даних чи живих стрімів кастомний профіль — саме те, що потрібно.
Тегування та інвалідація кешу
Ось де починається справжня магія. Система тегів дозволяє точково інвалідувати кеш після мутацій — дані оновлюються саме тоді, коли це потрібно, а не "раз на годину, бо так налаштовано".
Тегування за допомогою cacheTag
Функція cacheTag приймає один або кілька рядкових тегів. Думайте про теги як про мітки, за якими потім можна знайти й очистити потрібні записи кешу:
import { cacheTag, cacheLife } from 'next/cache'
export async function getUserOrders(userId: string) {
'use cache'
cacheTag(`user-${userId}-orders`, 'orders')
cacheLife('hours')
return await db.query(
'SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC',
[userId]
)
}
Тепер цей кеш можна інвалідувати як за конкретним користувачем (user-123-orders), так і глобально для всіх замовлень одразу (orders). Дуже зручно.
revalidateTag — фонова ревалідація
У Next.js 16 функція revalidateTag підтримує другий аргумент — профіль cacheLife, що активує stale-while-revalidate поведінку:
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(productId: string, data: FormData) {
await db.query(
'UPDATE products SET name = ?, price = ? WHERE id = ?',
[data.get('name'), data.get('price'), productId]
)
// Фонова ревалідація — поточний запит отримає старі дані,
// наступний — свіжі
revalidateTag(`product-${productId}`, 'max')
revalidateTag('products', 'max')
}
updateTag — миттєве оновлення
А якщо потрібно, щоб користувач одразу побачив свої зміни (класичний патерн read-your-own-writes)? Для цього є updateTag:
'use server'
import { updateTag } from 'next/cache'
export async function createComment(postSlug: string, content: string) {
await db.query(
'INSERT INTO comments (post_slug, content) VALUES (?, ?)',
[postSlug, content]
)
// Миттєва інвалідація — наступний запит гарантовано отримає свіжі дані
updateTag(`post-${postSlug}`)
}
Ключова різниця між ними: revalidateTag з профілем 'max' віддає старі дані, поки свіжі готуються у фоні. А updateTag негайно інвалідує кеш — наступний запит буде "чесною" ревалідацією. Обирайте залежно від того, що важливіше: швидкість відповіді чи актуальність даних.
Варіанти директиви: remote та private
Окрім базової 'use cache', є два спеціалізовані варіанти для специфічних сценаріїв. Розглянемо обидва.
«use cache: remote» — розподілений кеш
Стандартна 'use cache' зберігає дані в оперативній пам'яті кожного інстансу сервера. У безсерверних середовищах (serverless) це проблема — пам'ять не поділяється між інстансами, і ви отримуєте купу промахів кешу.
'use cache: remote' вирішує це, зберігаючи кеш у зовнішньому сховищі — Redis, Vercel KV або іншій key-value базі:
// lib/heavy-data.ts
import { cacheTag, cacheLife } from 'next/cache'
export async function getAnalyticsDashboard(orgId: string) {
'use cache: remote'
cacheTag(`analytics-${orgId}`)
cacheLife('minutes')
// Важкий запит, який не варто повторювати на кожному інстансі
const data = await computeAnalytics(orgId)
return data
}
Використовуйте 'use cache: remote', коли:
- Застосунок працює на кількох серверних інстансах
- Джерело даних не витримує високе навантаження конкурентних ревалідацій
- Потрібен єдиний шар кешу для всіх інстансів (а це, по суті, завжди в production)
«use cache: private» — кеш для користувацьких даних
Цей варіант поки що експериментальний, але вже дуже цікавий. Він дозволяє кешувати контент, що залежить від runtime-даних запиту — cookies, заголовків, пошукових параметрів:
// components/UserDashboard.tsx
'use cache: private'
import { cookies } from 'next/headers'
export default async function UserDashboard() {
const sessionToken = (await cookies()).get('session')?.value
const userData = await fetchUserData(sessionToken)
return (
<div>
<h2>Вітаємо, {userData.name}!</h2>
<p>Ваш баланс: {userData.balance}</p>
</div>
)
}
Важливий нюанс: результати 'use cache: private' ніколи не зберігаються на сервері. Вони кешуються лише в браузері й не переживають перезавантаження сторінки. Тримайте це на увазі при плануванні архітектури.
Поєднання з Suspense та PPR
Ось де 'use cache' розкриває весь свій потенціал. У поєднанні з React Suspense та Partial Prerendering кешований компонент формує статичну оболонку, а динамічний контент підвантажується потоково:
// app/dashboard/page.tsx
'use cache'
import { Suspense } from 'react'
import { cacheLife } from 'next/cache'
import StaticSidebar from './StaticSidebar'
import DynamicFeed from './DynamicFeed'
export default async function DashboardPage() {
cacheLife('hours')
return (
<div className="dashboard">
<StaticSidebar />
<Suspense fallback={<p>Завантаження стрічки...</p>}>
<DynamicFeed />
</Suspense>
</div>
)
}
StaticSidebar доставляється миттєво як частина статичної оболонки, а DynamicFeed завантажується асинхронно. Межа Suspense тут обов'язкова для динамічних компонентів при увімкненому cacheComponents — без неї отримаєте помилку.
Композиційні слоти: кешоване + динамічне
Є ще один прийом, який мені дуже подобається — передавати некешовані компоненти як props у кешований компонент. Це дає максимальну гнучкість:
// components/CachedLayout.tsx
'use cache'
import { cacheLife } from 'next/cache'
interface CachedLayoutProps {
header: React.ReactNode
children: React.ReactNode
}
export default async function CachedLayout({ header, children }: CachedLayoutProps) {
cacheLife('days')
const navigation = await getNavigation()
return (
<div>
{header}
<nav>
{navigation.map((item: any) => (
<a key={item.href} href={item.href}>{item.label}</a>
))}
</nav>
<main>{children}</main>
</div>
)
}
// app/page.tsx
import CachedLayout from '@/components/CachedLayout'
import DynamicHeader from './DynamicHeader'
import DynamicContent from './DynamicContent'
export default function HomePage() {
return (
<CachedLayout header={<DynamicHeader />}>
<DynamicContent />
</CachedLayout>
)
}
Навігація кешується, а header і children залишаються динамічними, бо передаються як React-вузли. Компілятор розуміє цю різницю автоматично.
Міграція з unstable_cache
Якщо ваш проєкт досі використовує unstable_cache — час мігрувати. На щастя, процес нескладний:
// ❌ Старий підхід (deprecated)
import { unstable_cache } from 'next/cache'
const getCachedPosts = unstable_cache(
async () => {
return await db.query('SELECT * FROM posts')
},
['posts'],
{ revalidate: 3600 }
)
// ✅ Новий підхід з 'use cache'
import { cacheTag, cacheLife } from 'next/cache'
export async function getCachedPosts() {
'use cache'
cacheTag('posts')
cacheLife('hours')
return await db.query('SELECT * FROM posts')
}
Що отримуєте від міграції? 'use cache' кешує не лише JSON-дані, а й компоненти та цілі маршрути. Ключі кешу генеруються автоматично. І, що найприємніше, код стає набагато чистішим — жодних обгорток та масивів ключів.
Обмеження та поширені помилки
Було б нечесно не згадати про підводні камені. Ось найпоширеніші проблеми, з якими стикаються розробники:
- Немає доступу до runtime API: кешовані функції не можуть напряму використовувати
cookies(),headers()чиsearchParams. Отримуйте ці значення зовні й передавайте як аргументи (виняток —'use cache: private'). - Серіалізація аргументів: всі аргументи мають бути серіалізовані, бо вони стають частиною ключа кешу. Не передавайте об'єкти з методами або циклічними посиланнями — отримаєте помилку на етапі компіляції.
- Кожне унікальне значення аргумента створює окремий запис кешу. Якщо передаєте об'єкт з 10 полями, а змінюється лише одне — кеш все одно промахнеться.
- Ліміти: максимальна довжина тегу — 256 символів, максимум 128 тегів на один запис.
// ❌ Помилка: прямий доступ до cookies всередині 'use cache'
export async function getUserData() {
'use cache'
const session = cookies().get('session') // Помилка!
return await fetchUser(session?.value)
}
// ✅ Правильно: передаємо значення як аргумент
export async function getUserData(sessionToken: string) {
'use cache'
cacheTag('user-data')
return await fetchUser(sessionToken)
}
Найкращі практики
На завершення — декілька порад, які (сподіваюсь) зекономлять вам час:
- Кешуйте стабільні дані: блог-пости, навігація, категорії — ідеальні кандидати. Ціни чи залишки на складі потребують частішого оновлення, тому обирайте короткі профілі.
- Віддавайте перевагу тегам над часовою ревалідацією: прив'язуйте
cacheTagі викликайтеupdateTagабоrevalidateTagу Server Actions. Це набагато точніше. - updateTag vs revalidateTag:
updateTag— для форм і коментарів (миттєве оновлення).revalidateTag(tag, 'max')— коли невелика затримка прийнятна. - Не змішуйте підходи: оберіть одну стратегію кешування для проєкту. Коли половина коду використовує часову ревалідацію, а інша — теги, дебаг перетворюється на кошмар.
- Починайте з рівня функції: це дає найбільшу гнучкість і перевикористовуваність. Переходьте до рівня компонента чи сторінки лише тоді, коли це реально спрощує архітектуру.
FAQ
Чи замінює «use cache» директиву revalidate у fetch()?
Так. У Next.js 16 з увімкненим cacheComponents опція next: { revalidate } у fetch() більше не потрібна. Використовуйте 'use cache' разом із cacheLife — це працює однаково для будь-якого джерела даних, чи то fetch, чи ORM, чи файлова система.
Чи можна використовувати «use cache» з Client Components?
Ні, директива працює лише в серверному контексті — Server Components, Server Actions та серверні функції. Але кешований Server Component може бути вкладеним у Client Component, а дані з кешованої функції можна передати як props. Тож обмеження не таке вже й суворе на практиці.
Яка різниця між «use cache» та ISR?
ISR був глобальним механізмом на рівні маршруту — увесь маршрут або статичний, або динамічний. 'use cache' дає гранулярний контроль: різні компоненти на одній сторінці можуть мати різний час кешування або взагалі не кешуватися. По суті, 'use cache' з PPR — це еволюція ISR, але значно гнучкіша.
Як налагоджувати кешування у розробці?
Використовуйте next dev --debug-cache — ця команда виводить попадання та промахи кешу прямо в консоль. Також у DevTools вкладка Network відображає кешовані відповіді з відповідними заголовками. Коли щось не кешується як очікувалося — починайте саме звідти.
Чи впливає «use cache» на SEO?
Позитивно. Кешований контент віддається швидше, що покращує Core Web Vitals (особливо LCP та TTFB). Пошукові боти отримують контент значно швидше, а це може покращити індексацію та позиції у видачі. Якщо SEO для вас важливе — кешування точно варте уваги.