Cache Components în Next.js 16: Ghid Complet pentru use cache, cacheLife și cacheTag

Învață cum să folosești use cache, cacheLife și cacheTag în Next.js 16 pentru un cache granular și predictibil. Cu exemple practice, profile personalizate și migrare de la unstable_cache.

Ce sunt Cache Components și de ce merită atenția ta

Hai să fim sinceri: dacă ai lucrat cu Next.js App Router înainte de versiunea 16, știi că sistemul de cache era... să zicem complicat. Fetch-urile erau cache-uite implicit, configurările globale se aplicau la nivel de segment, iar unstable_cache era singurul mod de a cache-ui interogări de bază de date sau alte operațiuni server-side. Un haos semi-controlat.

Ei bine, Next.js 16 schimbă radical toată această abordare prin introducerea Cache Components — un set de API-uri noi care fac cache-ul explicit, granular și, cel mai important, predictibil.

În centrul sistemului stă directiva 'use cache', alături de funcțiile cacheLife și cacheTag.

Ideea centrală e simplă: nimic nu mai e cache-uit implicit. Tu decizi ce se cache-uiește, pentru cât timp și cum se invalidează. Și asta funcționează nu doar cu funcții, ci și cu componente întregi și rute complete. Sincer, asta e una dintre cele mai bune schimbări din ecosistemul Next.js din ultimul an.

Activarea Cache Components în next.config.ts

Primul pas e simplu — activezi feature-ul în configurația proiectului. Fără acest flag, directivele 'use cache' pur și simplu nu vor funcționa:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

Acest singur flag deblochează atât Cache Components, cât și Partial Prerendering (PPR). Practic, activezi întregul sistem nou de rendering și cache cu o singură linie. Destul de elegant, nu?

Directiva use cache — fundația întregului sistem

Directiva 'use cache' se comportă similar cu 'use server' sau 'use client' — e o instrucțiune pentru compilator. O poți aplica la nivel de funcție, componentă sau fișier întreg.

La nivel de funcție

Cel mai granular mod de utilizare. Cache-uiești o singură funcție asincronă:

import { cacheTag, cacheLife } from 'next/cache'

export async function getProducts() {
  'use cache'
  cacheLife('hours')
  cacheTag('products')

  const products = await db.query('SELECT * FROM products')
  return products
}

Argumentele funcției devin automat parte din cheia de cache. Dacă apelezi getProducts() cu aceleași argumente, primești rezultatul din cache — fără să specifici manual chei. Compilatorul se ocupă de asta.

La nivel de componentă

Aici devine cu adevărat interesant. Poți cache-ui componente React Server întregi:

async function DashboardStats() {
  'use cache'
  cacheLife('hours')
  cacheTag('dashboard-stats')

  const stats = await db.stats.aggregate()
  return (
    <div className="grid grid-cols-3 gap-4">
      <StatCard title="Utilizatori" value={stats.users} />
      <StatCard title="Comenzi" value={stats.orders} />
      <StatCard title="Venituri" value={stats.revenue} />
    </div>
  )
}

Spre deosebire de unstable_cache (care putea cache-ui doar date JSON), 'use cache' poate cache-ui orice poate serializa React Server Components — inclusiv JSX complet. E o diferență enormă în practică.

La nivel de fișier

Adaugă directiva la începutul fișierului, și toate exporturile devin cache-abile:

'use cache'

import { cacheLife } from 'next/cache'

export async function getUser(id: string) {
  cacheLife('days')
  return db.users.findUnique({ where: { id } })
}

export async function getUserPosts(userId: string) {
  cacheLife('hours')
  return db.posts.findMany({ where: { userId } })
}

cacheLife — controlul precis al duratei de cache

Funcția cacheLife definește cât timp rămâne un rezultat în cache. Lucrează cu trei parametri cheie care controlează comportamentul pe client și pe server.

Cele trei proprietăți

  • stale — cât timp clientul folosește cache-ul local fără a verifica serverul (controlează router-ul client)
  • revalidate — după acest interval, următoarea cerere declanșează o reîmprospătare în fundal (stale-while-revalidate pe server)
  • expire — expirarea totală; după acest interval fără cereri, cache-ul e eliminat complet

Profile predefinite

Next.js 16 vine cu profile gata configurate pentru scenarii comune, ceea ce e foarte practic:

import { cacheLife } from 'next/cache'

// Conținut care se actualizează frecvent
async function LiveFeed() {
  'use cache'
  cacheLife('minutes')
  // stale: 60s, revalidate: 60s, expire: 300s
  return await fetchFeed()
}

// Articole de blog, produse
async function BlogPost({ slug }: { slug: string }) {
  'use cache'
  cacheLife('days')
  // stale: 86400s, revalidate: 86400s, expire: 604800s
  return await fetchPost(slug)
}

// Conținut aproape static
async function SiteConfig() {
  'use cache'
  cacheLife('max')
  // stale: 2592000s, revalidate: 2592000s, expire: indefinit
  return await fetchConfig()
}

Profile personalizate în next.config.ts

Când profilurile predefinite nu se potrivesc nevoilor tale (și se va întâmpla destul de des), poți defini profile proprii:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    editorial: {
      stale: 600,       // 10 minute — clientul servește din cache
      revalidate: 3600, // 1 oră — reîmprospătare în fundal
      expire: 86400,    // 1 zi — expirare totală
    },
    produs: {
      stale: 300,       // 5 minute
      revalidate: 1800, // 30 minute
      expire: 43200,    // 12 ore
    },
    cos_cumparaturi: {
      stale: 0,         // mereu verifică serverul
      revalidate: 60,   // reîmprospătare la fiecare minut
      expire: 3600,     // expiră după 1 oră
    },
  },
}

export default nextConfig

Apoi le folosești simplu, prin numele lor:

async function ProductPage({ id }: { id: string }) {
  'use cache'
  cacheLife('produs')
  return await fetchProduct(id)
}

Profile inline pentru cazuri unice

Pentru situații punctuale unde nu merită să creezi un profil reutilizabil:

async function getExchangeRates() {
  'use cache'
  cacheLife({
    stale: 30,        // 30 secunde
    revalidate: 300,  // 5 minute
    expire: 3600,     // 1 oră
  })
  return await fetchRates()
}

cacheTag — etichetarea și invalidarea granulară

Funcția cacheTag atașează etichete intrărilor din cache, permițând invalidarea țintită. Poți atașa mai multe etichete aceleiași intrări, ceea ce deschide o grămadă de posibilități:

import { cacheTag, cacheLife } from 'next/cache'

async function getProduct(id: string) {
  'use cache'
  cacheLife('days')
  cacheTag('products', `product-${id}`)

  return await db.products.findUnique({ where: { id } })
}

Acum poți invalida fie toate produsele (cu tag-ul 'products'), fie un produs specific (cu 'product-123'). Simplu și eficient.

Strategii de etichetare

O strategie bine gândită de etichetare face diferența între un cache eficient și unul care îți dă bătăi de cap:

// Etichete ierarhice — general spre specific
async function getUserProfile(userId: string) {
  'use cache'
  cacheTag('users', `user-${userId}`, `user-${userId}-profile`)
  // ...
}

async function getUserOrders(userId: string) {
  'use cache'
  cacheTag('orders', `user-${userId}`, `user-${userId}-orders`)
  // ...
}

// Invalidare: revalidateTag('users', 'max') → șterge TOATE profilurile
// Invalidare: updateTag(`user-${userId}`) → șterge doar datele unui utilizator

revalidateTag vs updateTag — când folosești fiecare

Next.js 16 oferă două moduri distincte de invalidare a cache-ului. Înțelegerea diferenței e esențială — am văzut destui developeri care le confundă, cu rezultate... neplăcute.

revalidateTag — invalidare eventuală (stale-while-revalidate)

revalidateTag marchează intrarea ca veche (stale), dar datele proaspete sunt generate abia la următoarea vizită a paginii. În Next.js 16, al doilea argument (un profil cacheLife) e obligatoriu:

import { revalidateTag } from 'next/cache'

// Într-un Route Handler sau Server Action
export async function POST() {
  // Marchează ca stale — datele proaspete se generează la următoarea vizită
  revalidateTag('products', 'max')

  return Response.json({ revalidated: true })
}

Atenție: Apelarea revalidateTag cu un singur argument (fără profil) este deprecată în Next.js 16. Folosește mereu al doilea argument.

updateTag — invalidare imediată (read-your-writes)

updateTag este disponibilă exclusiv în Server Actions și expiră imediat cache-ul. Următoarea cerere va aștepta datele proaspete, fără să servească conținut vechi:

'use server'

import { updateTag } from 'next/cache'

export async function updateProduct(id: string, data: FormData) {
  await db.products.update({
    where: { id },
    data: { name: data.get('name') as string },
  })

  // Expiră imediat — utilizatorul vede modificarea instant
  updateTag(`product-${id}`)
}

Pe scurt, când folosești care funcție

  • revalidateTag — pentru conținut static unde consistența eventuală e acceptabilă (articole de blog, pagini de categorie, dashboard-uri)
  • updateTag — pentru acțiuni ale utilizatorului unde acesta trebuie să vadă rezultatul imediat (editare profil, adăugare în coș, actualizare setări)

Cele trei directive de cache

Next.js 16 oferă trei variante ale directivei use cache, fiecare pentru un scenariu diferit. Merită să le înțelegi pe toate trei, chiar dacă în majoritatea proiectelor vei folosi doar prima.

use cache — cache implicit în memorie

Varianta standard. Datele sunt stocate în memoria serverului folosind un cache LRU (Least Recently Used). Ideal pentru shell-ul static al paginilor și date care nu au nevoie de persistență între instanțe:

async function NavigationMenu() {
  'use cache'
  cacheLife('days')
  const menu = await db.menus.findFirst({ where: { slug: 'main' } })
  return <nav>{/* render menu */}</nav>
}

use cache: remote — cache partajat între instanțe

Stochează datele într-un cache extern (Redis, KV store) partajat între toate instanțele serverless. Foarte util când ai mai multe replici ale aplicației:

async function getProductPrice(productId: string, currency: string) {
  'use cache: remote'
  cacheTag(`price-${productId}`)
  cacheLife({ expire: 3600 })

  return await db.products.getPrice(productId, currency)
}

Avantajul? Toți utilizatorii cu aceeași monedă partajează cache-ul. Dezavantajul? Latență adițională pentru lookup-ul în cache-ul remote (plus costuri de infrastructură). E un compromis pe care trebuie să-l evaluezi în funcție de proiect.

use cache: private — cache doar pe client

Varianta specială care permite accesul la API-uri runtime (cookies(), headers()) în interiorul scope-ului cache-uit:

import { cookies } from 'next/headers'
import { cacheTag, cacheLife } from 'next/cache'

async function getRecommendations(productId: string) {
  'use cache: private'
  cacheTag(`reco-${productId}`)
  cacheLife({ stale: 60 })

  const sessionId = (await cookies()).get('session-id')?.value || 'guest'
  return await fetchPersonalizedRecommendations(sessionId, productId)
}

De reținut: rezultatele nu sunt stocate pe server — cache-ul există doar în memoria browser-ului și nu persistă între reîncărcări de pagină. Deci nu te baza pe el pentru date critice.

Migrarea de la unstable_cache la use cache

Dacă ai un proiect existent care folosește unstable_cache, migrarea e surprinzător de directă. Hai să vedem comparația:

Înainte — cu unstable_cache

import { unstable_cache } from 'next/cache'

const getCachedProducts = unstable_cache(
  async () => {
    return await db.products.findMany()
  },
  ['products'],           // chei de cache manuale
  {
    revalidate: 3600,      // 1 oră
    tags: ['products'],   // etichete pentru invalidare
  }
)

După — cu use cache

import { cacheTag, cacheLife } from 'next/cache'

async function getProducts() {
  'use cache'
  cacheLife('hours')
  cacheTag('products')

  return await db.products.findMany()
}

Diferențele principale care fac migrarea să merite:

  • Nu mai e nevoie de funcții wrapper — adaugi directiva direct în funcție
  • Cheile de cache sunt generate automat de compilator pe baza argumentelor
  • Poți cache-ui componente întregi, nu doar date JSON
  • Sintaxa e mult mai curată și mai declarativă

Cache Handlers personalizate

Implicit, Next.js folosește un cache LRU în memorie. Asta funcționează bine local, dar în medii serverless (unde memoria nu e partajată între instanțe) ai nevoie de un storage extern. Aici intervin cacheHandlers:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheHandlers: {
    default: require.resolve('./cache-handlers/redis-handler.js'),
    remote: require.resolve('./cache-handlers/redis-handler.js'),
  },
}

export default nextConfig

Un cache handler trebuie să implementeze interfața CacheHandler cu metodele get și set. Pachetele existente precum @neshca/cache-handler sau @mrjasonroy/cache-components-cache-handler oferă implementări gata de producție pentru Redis, compatibile cu Next.js 16.

Un detaliu important: configurația cacheHandlers (plural) este pentru directivele 'use cache'. Configurația cacheHandler (singular) rămâne pentru cache-ul ISR și Route Handler. Nu le confunda — am făcut eu greșeala asta și mi-a luat o oră să-mi dau seama.

Greșeli frecvente și cum le eviți

1. Apelarea cookies() sau headers() în use cache

Directiva standard 'use cache' nu permite accesul la API-uri runtime. Dacă ai nevoie de cookies, extrage valoarea în afara scope-ului cache-uit și transmite-o ca argument:

// ❌ Greșit
async function getUserData() {
  'use cache'
  const session = await cookies() // Eroare!
  return await fetchUser(session.get('userId'))
}

// ✅ Corect
async function getUserData(userId: string) {
  'use cache'
  cacheTag(`user-${userId}`)
  return await fetchUser(userId)
}

// Apelul din componenta părinte:
export default async function Page() {
  const session = await cookies()
  const userId = session.get('userId')?.value
  return <UserProfile data={await getUserData(userId!)} />
}

2. Lipsa Suspense pentru conținut dinamic lângă conținut cache-uit

Dacă o componentă dinamică nu e învelită în <Suspense>, vei primi eroarea „Uncached data was accessed outside of Suspense". Soluția:

import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <div>
      <CachedStats />  {/* Componentă cu 'use cache' */}
      <Suspense fallback={<p>Se încarcă...</p>}>
        <LiveNotifications />  {/* Componentă dinamică */}
      </Suspense>
    </div>
  )
}

3. Argumentele ne-serializabile

Argumentele funcțiilor cache-uite trebuie să fie serializabile. Nu poți transmite instanțe de clase, funcții sau alte valori complexe. Folosește doar tipuri primitive, obiecte simple și array-uri. E o limitare cu care trebuie să te obișnuiești.

4. Cache-ul nu se invalidează în medii serverless

Cache-ul LRU implicit nu e partajat între instanțe serverless — fiecare instanță are propriul cache, ceea ce duce la inconsistențe. Soluția: configurează un cacheHandler extern (Redis, Upstash) prin cacheHandlers în configurație.

5. revalidateTag fără al doilea argument

În Next.js 16, apelarea revalidateTag('products') fără un profil cacheLife e deprecată și va genera un avertisment. Adaugă mereu profilul:

// ❌ Deprecat
revalidateTag('products')

// ✅ Corect
revalidateTag('products', 'max')

Pattern practic: Dashboard cu cache mixt

Hai să punem totul cap la cap cu un exemplu concret — un dashboard cu secțiuni cache-uite diferit:

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { CachedStats } from './components/stats'
import { RecentOrders } from './components/recent-orders'
import { LiveAlerts } from './components/live-alerts'

export const experimental_ppr = true

export default function DashboardPage() {
  return (
    <div className="grid grid-cols-12 gap-6">
      {/* Shell-ul static — servit instant de pe CDN */}
      <header className="col-span-12">
        <h1>Dashboard</h1>
      </header>

      {/* Statistici — cache-uite pe ore */}
      <section className="col-span-8">
        <CachedStats />
      </section>

      {/* Comenzi recente — cache-uite pe minute */}
      <section className="col-span-4">
        <Suspense fallback={<OrdersSkeleton />}>
          <RecentOrders />
        </Suspense>
      </section>

      {/* Alerte live — fără cache, streaming */}
      <section className="col-span-12">
        <Suspense fallback={<AlertsSkeleton />}>
          <LiveAlerts />
        </Suspense>
      </section>
    </div>
  )
}
// app/dashboard/components/stats.tsx
import { cacheLife, cacheTag } from 'next/cache'

export async function CachedStats() {
  'use cache'
  cacheLife('hours')
  cacheTag('dashboard-stats')

  const [users, orders, revenue] = await Promise.all([
    db.users.count(),
    db.orders.count({ where: { createdAt: { gte: startOfMonth() } } }),
    db.orders.aggregate({ _sum: { total: true } }),
  ])

  return (
    <div className="grid grid-cols-3 gap-4">
      <StatCard title="Utilizatori" value={users} />
      <StatCard title="Comenzi luna aceasta" value={orders} />
      <StatCard title="Venituri" value={formatCurrency(revenue)} />
    </div>
  )
}
// app/dashboard/actions.ts
'use server'

import { updateTag } from 'next/cache'

export async function refreshDashboard() {
  updateTag('dashboard-stats')
}

Întrebări frecvente

Care e diferența între use cache și ISR tradițional?

ISR (Incremental Static Regeneration) funcționează la nivel de rută întreagă — fie întreaga pagină e cache-uită, fie nu. Cache Components cu 'use cache' oferă granularitate la nivel de componentă sau funcție. Poți avea o pagină unde antetul e cache-uit pe zile, conținutul principal pe ore, iar sidebar-ul e complet dinamic. Plus, funcționează cu Partial Prerendering (PPR) pentru a combina conținut static și dinamic în aceeași rută.

Pot folosi use cache cu Client Components?

Nu, din păcate. Directiva 'use cache' funcționează exclusiv cu funcții și componente server-side. Nu poți aplica 'use cache' unei componente marcate cu 'use client'. Totuși, o componentă cache-uită poate returna JSX care include Client Components — cache-ul se aplică output-ului serverului, nu interactivității clientului.

Ce se întâmplă cu cache-ul în mediul de dezvoltare?

În modul next dev, cache-ul funcționează diferit față de producție — datele sunt revalidate la fiecare cerere pentru a facilita debugging-ul. Dacă vrei să testezi comportamentul real, rulează next build urmat de next start. Altfel, te vei întreba de ce cache-ul „nu funcționează".

Cum aleg între use cache, use cache: remote și use cache: private?

Folosește 'use cache' (standard) pentru majoritatea cazurilor — merge bine pentru shell-ul static și date partajate între utilizatori. Alege 'use cache: remote' când ai mai multe instanțe serverless și vrei ca toate să partajeze același cache (necesită Redis sau similar). Folosește 'use cache: private' doar când ai nevoie de cookies() sau headers() în interiorul funcției — dar reține că rezultatele sunt cache-uite doar în browser.

updateTag funcționează în Route Handlers?

Nu. updateTag este disponibil exclusiv în Server Actions. Pentru invalidarea cache-ului din Route Handlers, webhook-uri sau alte contexte server-side, folosește revalidateTag('tag', 'max') care oferă comportament stale-while-revalidate.

Despre Autor Editorial Team

Our team of expert writers and editors.