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

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

Якщо ви працювали з кешуванням у попередніх версіях 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 для вас важливе — кешування точно варте уваги.

Про Автора Editorial Team

Our team of expert writers and editors.