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

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.

Codice di supporto 🔗


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.

  1. 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. grafico del punto 1.

  2. 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. grafico del punto 2.

  3. 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 di max-age. Non è necessario aggiornare ciò che l’utente sta vedendo. grafico del punto 3.

  4. 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" }'
  1. 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 un 200 OK e fornisce un nuovo ETag (che sovrascrive l’entrata precedente nella cache). grafico del punto 5.

🔑 messaggio Anche se il contenuto visualizzato è obsoleto, il browser aggiorna silenziosamente la sua cache, preservando l’esperienza utente.

  1. 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.