Next.js 16: Директива «use cache» — практичний посібник із кешування

Розбираємо директиву «use cache» в Next.js 16 — від увімкнення cacheComponents до стратегій інвалідації з cacheTag, cacheLife, revalidateTag та updateTag. Приклади коду, варіанти remote та private, міграція з unstable_cache.

Next.js 16 use cache: Повний гід 2026

Якщо ви працювали з кешуванням у попередніх версіях 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)
}

Найкращі практики

На завершення — декілька порад, які (сподіваюсь) зекономлять вам час:

  1. Кешуйте стабільні дані: блог-пости, навігація, категорії — ідеальні кандидати. Ціни чи залишки на складі потребують частішого оновлення, тому обирайте короткі профілі.
  2. Віддавайте перевагу тегам над часовою ревалідацією: прив'язуйте cacheTag і викликайте updateTag або revalidateTag у Server Actions. Це набагато точніше.
  3. updateTag vs revalidateTag: updateTag — для форм і коментарів (миттєве оновлення). revalidateTag(tag, 'max') — коли невелика затримка прийнятна.
  4. Не змішуйте підходи: оберіть одну стратегію кешування для проєкту. Коли половина коду використовує часову ревалідацію, а інша — теги, дебаг перетворюється на кошмар.
  5. Починайте з рівня функції: це дає найбільшу гнучкість і перевикористовуваність. Переходьте до рівня компонента чи сторінки лише тоді, коли це реально спрощує архітектуру.

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 для вас важливе — кешування точно варте уваги.

Про Автора Priya Raghavan

Priya spent four years on Vercel's developer experience team, where she wrote a chunk of the Turbopack incremental cache layer and rewrote the official App Router migration codemods used by tens of thousands of teams. Before that, she shipped the checkout funnel rewrite at Shopify on Next.js 12, cutting LCP from 3.4s to 1.1s on mid-tier Android. These days she runs an independent consultancy helping Series B companies untangle Pages Router monoliths into RSC-first App Router architectures. She has opinions about Server Actions, mostly negative ones, and is slowly coming around on Partial Prerendering after benchmarking it against six production workloads. She writes about cache semantics, streaming SSR, and the parts of the React docs that quietly contradict the Next.js docs. Eight years in frontend, three of them spent yelling at hydration mismatches.