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.