Vai al contenuto
Web

La cache di Next.js 16: use cache, cacheLife e updateTag

In Next.js 16 niente va in cache finché non scrivi 'use cache'. Come lavorano i sette profili di cacheLife, cacheTag e updateTag, e cosa cambia per fetch.

7 ottobre 2026 · 10 min di lettura

A grid of twelve dark tiles with six lit amaranth, each lit tile circled by a progress ring of a different length; three threads run from the right column of tiles to one lit pill on the right, and the middle tile of that column glows brighter with a full ring

Il modello di cache di Next.js 16 è un sistema opt-in: niente finisce in cache finché non marchi una funzione, un componente o un file con la direttiva 'use cache', e ogni voce in cache ha una durata fissata da cacheLife e, se serve, dei tag da invalidare dopo una scrittura. Al posto delle cache implicite delle prime versioni dell'App Router ci sono una direttiva e poche funzioni. Si attiva con un solo flag, cacheComponents: true, e lo stesso flag rende il Partial Prerendering il modo predefinito di renderizzare una route.

Riguarda ogni team che lavora con l'App Router e si è chiesto perché una pagina mostrava dati vecchi, o perché una pagina pensata come statica veniva renderizzata a ogni richiesta. Le due domande avevano la stessa causa: decideva il framework cosa mettere in cache, e la decisione era difficile da vedere. In Next.js 16 la decisione sta nel codice, accanto ai dati a cui si applica. Qui descriviamo il modello come lo documenta Next.js 16.4, uscito il 6 ottobre 2026.

La versione in trenta secondi

  • Dinamico per default. Con Cache Components attivo, ogni fetch e ogni query al database gira al momento della richiesta, a meno che non stia dentro uno scope 'use cache'.
  • Una durata per ogni voce. cacheLife('hours'), o un profilo tuo, imposta tre tempi: quanto a lungo il browser riusa la voce, quando il server la rigenera in background e quando scade.
  • Invalidazione per tag. cacheTag('posts') etichetta una voce. Dopo una scrittura, con updateTag('posts') la lettura successiva aspetta i dati freschi; con revalidateTag('posts', 'max') si serve la copia vecchia mentre se ne prepara una nuova.
  • La shell statica nasce dalla cache. L'output in cache con una durata abbastanza lunga entra nel prerender della route. Le voci di breve durata diventano buchi che si riempiono al momento della richiesta.

Perché Next.js ha cambiato di nuovo il modello di cache?

Perché i default sorprendevano. In Next.js 14 fetch usava force-cache salvo indicazione contraria, le route handler GET finivano in cache e il router lato client teneva in memoria i segmenti di pagina tra una navigazione e l'altra. Una pagina poteva servire dati vecchi senza che una sola riga di codice lo dicesse. Next.js 15 ha ribaltato quei default: fetch, handler GET e navigazioni lato client non vanno più in cache se non lo chiedi tu.

La sorpresa è sparita, ma gli strumenti sono rimasti sparsi: l'opzione cache di fetch, next.revalidate, le configurazioni di segmento dynamic, revalidate e fetchCache, e unstable_cache per tutto ciò che non era una fetch. Next.js 16, uscito a ottobre 2025, li ha riuniti in una sola direttiva. Con Cache Components attivo, la guida ufficiale alla migrazione chiede di togliere dynamic, revalidate e fetchCache dalle route e di spostare la scelta sulla cache direttamente sui dati.

Come funziona 'use cache'?

'use cache' marca come memorizzabile una funzione async, un componente async o un intero file. La prima chiamata con certi input esegue il corpo e salva il risultato. Le chiamate successive con gli stessi input lo riusano, dentro lo stesso render e tra richieste diverse, finché la voce non scade.

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

export async function getProject(id: string) {
  'use cache'
  cacheLife('hours')
  cacheTag('projects', `project-${id}`)
  return db.project.findUnique({ where: { id } })
}

La chiave di cache si compone di quattro elementi: l'ID della build, un hash della posizione e della firma della funzione, gli argomenti serializzati e le variabili che la funzione cattura dallo scope esterno. Ne seguono due conseguenze. Argomenti e valori di ritorno devono essere serializzabili, quindi niente istanze di classe e niente connessioni aperte. E ogni nuovo deploy parte con la cache vuota, perché l'ID della build entra in ogni chiave.

Uno scope in cache non può leggere cookies(), headers() o searchParams, e il divieto segue lo stack delle chiamate: anche un helper che legge un cookie fallisce se lo chiami da una funzione in cache. La documentazione avverte che su una route dinamica l'errore può passare indenne next build e comparire solo con next start. La soluzione è leggere i dati della richiesta fuori e passarli come argomento; così entrano anche nella chiave.

import { cookies } from 'next/headers'

export default async function Page() {
  const teamId = (await cookies()).get('team')?.value ?? ''
  const projects = await getProjects(teamId) // cached per teamId
  return <ProjectList projects={projects} />
}

Quanto dura una voce in cache?

cacheLife imposta tre tempi. stale è quanto a lungo il browser riusa la voce senza interrogare il server. revalidate è il momento in cui la richiesta successiva avvia una rigenerazione in background, ricevendo comunque la copia in cache. expire è il momento in cui una voce che nessuno ha richiesto viene scartata, e la richiesta successiva aspetta l'output fresco. Next.js offre sette profili predefiniti:

  • default: stale 5 minuti, revalidate 15 minuti, nessuna scadenza. Vale quando uno scope non chiama cacheLife.
  • seconds: stale 30 secondi, revalidate 1 secondo, expire 1 minuto.
  • minutes: stale 5 minuti, revalidate 1 minuto, expire 1 ora.
  • hours: stale 5 minuti, revalidate 1 ora, expire 1 giorno.
  • days: stale 5 minuti, revalidate 1 giorno, expire 1 settimana.
  • weeks: stale 5 minuti, revalidate 1 settimana, expire 30 giorni.
  • max: stale 5 minuti, revalidate 30 giorni, expire 1 anno.

Ogni profilo si può ridefinire, e se ne possono aggiungere di nuovi con un nome in next.config.ts. La documentazione consiglia di chiamare cacheLife in ogni scope in cache, e siamo d'accordo: un default implicito è la prima cosa che nessuno ricorda sei mesi dopo.

La durata decide anche dove può finire l'output. Una voce con revalidate a 0 o expire sotto i 5 minuti resta fuori dal prerender e diventa un buco dinamico. Resta fuori anche una voce con stale sotto i 30 secondi, perché il prefetch scadrebbe prima del clic. Tra i profili predefiniti, solo seconds supera queste soglie. Lato client, il router tiene ogni voce per almeno 30 secondi, qualunque sia la configurazione.

Come si invalida la cache dopo una scrittura?

La scadenza a tempo va bene per i contenuti che cambiano poco a poco. Per i dati che un utente ha appena modificato serve un'invalidazione su richiesta, e Next.js 16 ne offre tre, ognuna con una garanzia diversa:

  • updateTag(tag) fa scadere subito tutte le voci con quel tag, e la lettura successiva aspetta i dati freschi. Funziona solo dentro le Server Action. È lo strumento per i form: l'utente rinomina un progetto e vede il nome nuovo, non il vecchio.
  • revalidateTag(tag, 'max') segna le voci come scadute. La richiesta successiva riceve ancora la copia vecchia mentre in background se ne prepara una fresca. Funziona nelle Server Action e nelle Route Handler. È lo strumento per webhook e pubblicazioni dal CMS, dove qualche secondo di contenuto vecchio non costa niente. Il secondo argomento ora è obbligatorio; la forma con un solo argomento è deprecata.
  • refresh() non tocca nessuna voce in cache. Da una Server Action chiede al router lato client di ricaricare la pagina corrente, per i dati che non sono mai stati in cache.
'use server'
import { updateTag } from 'next/cache'

export async function renameProject(id: string, name: string) {
  await db.project.update({ where: { id }, data: { name } })
  updateTag(`project-${id}`)
}
import { revalidateTag } from 'next/cache'

export async function POST(request: Request) {
  const { slug } = await request.json()
  revalidateTag(`post-${slug}`, 'max')
  return Response.json({ ok: true })
}

Il lavoro di progettazione vero sta nei tag. cacheTag accetta più valori: conviene etichettare ogni voce con la sua collezione (projects) e con il suo record (project-42). Una lista si invalida con il primo, una pagina di dettaglio con il secondo. I tag distinguono maiuscole e minuscole e hanno un limite di 256 caratteri; un tag più lungo non viene mai assegnato, quindi invalidarlo non fa niente e non segnala errori.

E fetch, adesso?

Con Cache Components attivo, fetch non va in cache a meno che non giri dentro uno scope 'use cache'. La guida alla migrazione sposta cache: 'force-cache' in una funzione in cache, next.revalidate in cacheLife e next.tags in cacheTag. unstable_cache continua a funzionare, quindi una codebase grande può migrare una funzione di dati alla volta.

C'è una differenza che sfugge facilmente. La cache di fetch e unstable_cache possono sopravvivere a un deploy; le voci di 'use cache' no, perché l'ID della build sta nella chiave. Se fai dieci deploy al giorno contro un'API esterna lenta, ogni deploy riparte a freddo. Per i dati che devono persistere tra un deploy e l'altro, la documentazione indica la cache di fetch.

Dove sta la cache a runtime?

Di default in memoria, in uno store LRU dentro il processo del server. Quanto serva dipende da dove ospiti l'app. Su un server sempre acceso le voci restano tra una richiesta e l'altra, e cacheMaxMemorySize ne limita la dimensione. Su serverless ogni richiesta può finire su un'istanza diversa, quindi le voci create a runtime spesso si perdono; la cache creata in build continua ad alimentare la shell statica. Abbiamo confrontato questi compromessi in Next.js self-hosted con Docker o Vercel.

Due varianti della direttiva coprono i casi scoperti. 'use cache: remote' salva le voci in un cache handler fornito dalla piattaforma, come Redis o uno store KV, e funziona anche dopo aver letto i dati della richiesta. Costa un round trip di rete a ogni lettura e di solito una tariffa della piattaforma. 'use cache: private' può leggere cookie e header e tiene i risultati solo nella memoria del browser, mai sul server. La documentazione lo riserva a esigenze di compliance o a codice che non si può rifattorizzare per passare i dati della richiesta come argomento.

Cache Components richiede anche il runtime Node.js. Una route che esporta ancora runtime = 'edge' va spostata prima di attivare il flag, e questo chiude una delle domande del nostro confronto tra edge runtime e Node runtime per ogni progetto App Router che adotta il modello.

Che rapporto c'è tra cache e Partial Prerendering?

Sono lo stesso meccanismo visto da due lati. In build Next.js renderizza tutto quello che può: il markup statico e ogni scope in cache con una durata abbastanza lunga. Quell'output diventa la shell servita dalla CDN. I dati non in cache e le API legate alla richiesta stanno dentro boundary Suspense e arrivano in streaming nella stessa risposta. Scegliere un cacheLife vuol dire scegliere quanta parte della pagina arriva subito. Il lato delle route è nella nostra guida su come strutturare le route per il Partial Prerendering.

Le regole che seguiamo su una codebase SaaS

  1. In cache le funzioni di dati, non le pagine. Un 'use cache' in testa a un file di pagina mette in cache tutto quello che c'è sotto, compreso ciò che qualcuno aggiungerà il trimestre prossimo. Un getProject(id) in cache resta stretto quanto il suo nome.
  2. cacheLife in ogni scope. Le cache annidate senza una durata esplicita ereditano quella breve di una cache interna; in quel caso Next.js lancia un errore durante il prerender, e una durata dichiarata lo evita in partenza.
  3. Tag per collezione e per record. Due tag per voce coprono la lista e la pagina di dettaglio.
  4. updateTag nelle action, revalidateTag(tag, 'max') nei webhook. Chi ha fatto la modifica la vede subito; tutti gli altri la vedono alla richiesta successiva. È la stessa divisione che usiamo nei nostri pattern per le Server Action.
  5. I dati della richiesta restano fuori. Il cookie si legge nella pagina, e l'ID del team si passa alla funzione in cache.
  6. 'use cache: remote' solo quando l'origine non regge il carico. Ogni lettura diventa una chiamata di rete: si accetta più latenza in cambio di meno query al database.

Quando il modello costa più di quanto fa risparmiare

Attivare cacheComponents su un'app esistente è una migrazione. Tutto diventa dinamico finché non lo marchi, quindi un sito che contava sui vecchi default prima rallenta e poi accelera. La validazione in build segnala ogni route che legge dati della richiesta fuori da un boundary Suspense, e su una codebase App Router grande risolverli è un lavoro vero, da fare route per route. Un'app interamente personale, senza dati condivisi, ci guadagna poco: la cache non ha niente da condividere tra le richieste. Per un sito marketing con contenuti che cambiano ogni settimana, invece, il modello calza bene: cacheLife('days') sulle funzioni dei contenuti, un tag per voce e un webhook che chiama revalidateTag a ogni pubblicazione.

Domande frequenti

Articoli correlati

Studio

Inizia un progetto.

Scriviamo riguardo a ciò che costruiamo. Raccontaci cosa vuoi costruire tu.