Cache-Control max-age, stale-while-revalidate

Scritto da Francesco Di Donato • 23 novembre 2023 • 5 minuti di lettura
Fino ad ora, grazie a Last-Modified/If-Modified-Since
o ETag/If-None-Match
abbiamo principalmente risparmiato sulla larghezza di banda. Tuttavia, il server ha sempre dovuto elaborare ogni richiesta.
Il server può istruire il client riguardo l’uso delle risorse memorizzate per una certa durata, decidendo se e quando il client dovrebbe ri-validare il contenuto e se farlo o meno in background.
Gli endpoint nel mondo reale non sono così istantanei come quelli in questo tutorial. La risposta può richiedere millisecondi per essere generata, senza nemmeno considerare la posizione del server rispetto al richiedente!
Aumentiamo l’asynchronicità tra server e client per evidenziare la necessità dell’intestazione Cache-Control
.
export async function sleep(duration) {
return new Promise((resolve) => setTimeout(resolve, duration));
}
Sulla base del precedente endpoint /only-etag
🔗, registra /cache-control-with-etag
. Per ora, è identico, tranne che aspetta tre secondi prima di rispondere.
Inoltre, aggiungi alcuni log prima di inviare la risposta
export default async (req, res) => {
console.info("Caricamento sul server!");
work; // computazione;
await sleep(3000) // simula async.
const etag = createETag(html);
res.setHeader("ETag", etag);
const ifNoneMatch = new Headers(req.headers).get("If-None-Match");
if (ifNoneMatch === etag) {
res.writeHead(304).end();
return;
}
res.writeHead(200).end(html);
};
Visualizziamo il problema. Quando richiedi una pagina dal browser, entra in uno stato di caricamento per tre secondi. Anche se aggiorni entro quel tempo, il browser fa di nuovo la richiesta, indipendentemente dai meccanismi ETag
o Last-Modified
. Il contenuto della pagina persiste perché in sostanza rimani sulla stessa pagina. Per osservare il comportamento più chiaramente, prova a riaprire la pagina da una nuova scheda o a partire da un sito diverso.
La cosa più importante è che il server viene contattato ad ogni richiesta!
max-age
È possibile istruire il browser ad utilizzare la versione memorizzata per una certa durata. Il server imposterà l’intestazione di risposta Cache-Control
con un valore di max-age=<seconds>
.
export default async (req, res) => {
console.info("Caricamento sul server!");
work; // recupero dal db e templating;
await sleep(3000) // simula async.
// Istruire il browser a usare la risorsa memorizzata
// per 60 * 60 secondi = 1 ora
res.setHeader("Cache-Control", "max-age: 3600");
etag; // come visto prima
// 200 o 304
};
Riprovaci ora e richiedi la pagina. La prima richiesta costringe il browser a caricare per tre secondi. Se apri la stessa pagina in un’altra scheda, noterai che è già lì, e il server non è stato contattato.
Questo comportamento persiste per i secondi specificati. Se, dopo la scadenza della cache, una nuova richiesta restituisce un 304 Not Modified
(grazie all’intestazione If-None-Match
), la risorsa verrà nuovamente memorizzata per quel periodo di tempo.
- ➕ Migliore UX
- ➕ Meno carico sul server
- ❔ Se la risorsa cambia, il client rimane all’oscuro fino a quando la cache non scade. Dopo la scadenza, con un Etag diverso, verrà visualizzata e memorizzata una nuova versione.
Quando tenti un aggiornamento mentre sei sulla pagina, potresti osservare il ritardo di caricamento, indicando che il server viene contattato. Assicurati di non fare un hard refresh, poiché questo sovrascrive il comportamento descritto.
stale-while-revalidate
Se la tua applicazione ha bisogno di validazione delle risorse ma vuoi comunque mostrare la versione memorizzata per una migliore esperienza utente quando disponibile, puoi utilizzare la direttiva stale-while-revalidate=<seconds>
.
res.setHeader("Cache-Control",
"max-age=120, stale-while-revalidate=300"
);
In questo caso, al browser viene istruita a memorizzare la risposta per 2 minuti. Una volta scaduto questo periodo, se la risorsa viene richiesta entro i successivi 5 minuti, il browser utilizzerà la risorsa memorizzata (anche se obsoleta) ma eseguirà una chiamata di validazione in background.
Voglio sottolineare il “anche se obsoleta” giocando con l’endpoint configurato come sopra. Solo refresh morbidi vengono eseguiti.
ℹ️ Informazioni Tocca/clicca sugli elementi successivi per mostrare il grafico correlato.
-
Alla richiesta iniziale della pagina, ci vogliono 3 secondi per caricarsi e la risposta viene memorizzata per 2 minuti con un ETag associato. Il client includerà questo ETag nell’intestazione
If-None-Match
per le richieste successive. -
Chiudi e riapri il browser (o richiedi da un’altra pagina) entro la finestra di 2 minuti: la pagina viene mostrata immediatamente senza contattare il server.
-
Dopo la scadenza della cache di 2 minuti, il server viene contattato. La risorsa non è cambiata, il client riceve
304 Not Modified
. La data di scadenza della cache viene estesa con il valore fornito dimax-age
. Non è necessario aggiornare ciò che l’utente sta vedendo. -
Ora che la finestra di 2 minuti è di nuovo aperta, aggiorniamo il contenuto della risorsa utilizzando l’endpoint db implementato nel precedente post del blog 🔗. Fondamentalmente, la prossima volta l’ETag non corrisponderà.
curl -X POST http://127.0.0.1:8000/db \
-d '{ "title": "ETag", "tag": "code" }'
- Ancora entro la finestra di 2 minuti, richiedi di nuovo la pagina. Grazie a
max-age
, il browser la mostra immediatamente. Procede con la validazione in background come già visto nel passo 3. Ma questa volta il ETag non corrisponde; il server risponde con un200 OK
e fornisce un nuovo ETag (che sovrascrive l’entrata precedente nella cache).
🔑 messaggio Anche se il contenuto visualizzato è obsoleto, il browser aggiorna silenziosamente la sua cache, preservando l’esperienza utente.
- Richiedi di nuovo la pagina; questa volta, visualizza finalmente l’ultima versione memorizzata. Se entro la nuova finestra di 2 minuti, il server non sarà contattato.
🔑 messaggio Qualsiasi richiesta al di fuori della finestra di tempo indicata da stale-while-revalidate (che ricordo, inizia dalla scadenza di quella di max-age), si comporterà come nel passo 1 - stato vuoto.