Headers per file di grandi dimensioni

Headers per file di grandi dimensioni

Scritto da Francesco Di Donato 26 aprile 2022 5 minuti di lettura

Quando hai molte informazioni da mostrare sullo schermo, ci sono due scelte comuni:

Ma sai una cosa? Puoi anche inviare tutto all’utente. Esploriamo insieme questo approccio diretto.

Headers

Esploreremo tre headers cruciali - Content-Length, Content-Encoding e Transfer-Encoding - e vedremo come diverse combinazioni di questi headers possono influenzare il rendering nel browser.

Content-Length 🔗

Specifica la dimensione del payload in byte. Aiuta il destinatario (nel nostro caso, il browser) a sapere quanta data aspettarsi. È come un avviso, garantendo che tutti siano sulla stessa lunghezza d’onda riguardo la quantità di contenuto in arrivo.

Content-Encoding 🔗

Indica al browser come il contenuto è codificato o compresso. È come un codice segreto che sia il server che il browser comprendono. Le codifiche comuni includono gzip e deflate. Quando il browser vede questa intestazione, sa come decodificare il contenuto per una visualizzazione fluida.

Transfer-Encoding 🔗

Questa intestazione meno conosciuta gestisce la codifica del messaggio stesso durante la trasmissione. Può essere inviata tutta in una volta (come un unico grande pacco) o in pezzi più piccoli (come suddividerla in vari pacchi).


Server

Per vedere come il browser reagisce a diverse combinazioni di queste headers, impostiamo un server HTTP Node.js di base.

Il server é progettato per gestire le richieste in arrivo per qualsiasi percorso, offrendo flessibilità attraverso parametri di query opzionali:

  • content-length: Se impostato su true, questo parametro aggiunge l’intestazione Content-Length alla risposta.

  • transfer: Questo parametro imposta l’intestazione Transfer-Encoding, con opzioni per:

    • identity: Istrua il server a inviare il documento per intero.
    • chunked: Indica al server di inviare il documento in parti.
  • gzip: Se impostato su true, il server recupera la versione compressa del file. In questo caso, il Content-Length (se impostato) riflette la dimensione della versione compressa.

Support Code 🔗

import { createServer } from "http";
import { getView, combinations } from "./src/utils.mjs";

createServer(async (req, res) => {
  const { searchParams: sp } = new URL(req.url, "http://127.0.0.1:8000");

  // Parametri di query pronti
  const length = sp.get("content-length") === "true" || false;
  const gzip = sp.get("gzip") === "true" || false;
  const identity = sp.get("identity") === "true" || false;

  // O index.html o index.html.gz
  let view = "index.html";
  if (gzip) view += ".gz";

  const [data, stats, _] = await getView(view);

  res.setHeader("Content-Type", "text/html");

  if (length) res.setHeader("Content-Length", stats.size);

  if (gzip) res.setHeader("Content-Encoding", "gzip");

  if (identity) res.setHeader("Transfer-Encoding", "identity")

  return res.writeHead(200).end(data);
}).listen(8000, "127.0.0.1", () => {
  console.info(combinations().join("\n"));
});
import { resolve } from "path";
import { readFile, stat } from "fs/promises";

const root = resolve(new URL(".", import.meta.url).pathname, "..");

export async function getView(name, encoding) {
  const filepath = resolve(root, "src", "views", name);

  const [data, stats] = await Promise.allSettled([
    readFile(filepath, encoding),
    stat(filepath),
  ]);
  if (data.status === "rejected")
    return [null, null, new Error("impossibile recuperare i dati")];
  if (stats.status === "rejected")
    return [null, null, new Error("impossibile recuperare le statistiche")];

  return [data.value, stats.value, null];
}

// Ignora se non viene eseguito sulla tua macchina.
export function createURL(
  { gzip, length, identity } = {
    gzip: false,
    length: false,
    identity: false,
  }
) {
  // Usa il pathname in modo che negli Strumenti per sviluppatori tu possa filtrare il favicon.
  const url = new URL("big", "http://127.0.0.1:8000");
  if (gzip) url.searchParams.set("gzip", "true");
  if (length) url.searchParams.set("content-length", "true");
  if (identity) url.searchParams.set("identity", "true");
  return url.toString();
}

// Ignora se non viene eseguito sulla tua macchina.
export function combinations() {
  const urls = [createURL()];
  for (const gzip of [false, true]) {
    for (const length of [false, true]) {
      for (const identity of [false, true]) {
        urls.push(
          createURL({
            gzip,
            length,
            identity,
          })
        );
      }
    }
  }
  return urls;
}

Il contenuto all’interno dei file HTML non è il focus; la loro sostanziale dimensione è (AKA sono GRANDI).

Questi file sono mantenuti in due versioni: normale e gzipped. Anche se generalmente non è una pratica raccomandata per i server reali adottare una doppia archiviazione, qui l’intento è evitare di introdurre un overhead aggiuntivo nel caso della compressione.


Testing

Ora testerò il server usando Firefox, monitorando come i tempi di risposta variano con i cambiamenti nelle intestazioni. Se stai seguendo da casa, assicurati di disabilitare la cache e considera di spuntare la casella ‘Preserva registri’ per un’osservazione più accurata.

Screenshot della scheda Rete che mostra ciò che è descritto di seguito.

Inizialmente, facciamo una richiesta senza parametri di query, lasciando che il server HTTP Node.js la gestisca per impostazione predefinita. L’impostazione predefinita del Transfer-Encoding osservata è chunked, in linea con il comportamento di passare ?transfer=chunked. Node.js punta ad essere non bloccante, e questa scelta garantisce un’elaborazione più fluida.

Ora, rendiamo le cose più interessanti passando il parametro di query ?transfer=identity. Questa volta, la richiesta impiega notevolmente più tempo per completarsi.

Per rimediare a questo, introduciamo l’intestazione Content-Length con ?content-length=true&identity=true, risultando in una riduzione significativa della durata. È come spedire un pacco tutto in una volta. Includere l’intestazione Content-Length è la nota amichevole che dice: ‘Ehi, il tuo pacco è grande così!’. Senza di essa, il client potrebbe barcollare nel tentativo di indovinare la dimensione, portando a momenti di elaborazione dei dati imbarazzanti.

🔑 punto chiave In modalità identity, sii un buon server e allega sempre quell’intestazione Content-Length.

Come osservazione finale, notiamo che la presenza del Content-Length non ha alcun impatto quando il metodo di trasferimento è impostato su chunked.

🔑 punto chiave Non solo non c’è bisogno dell’intestazione Content-Length, ma utilizzare entrambe è in realtà contraddittorio. In codifica ‘chunked’, la dimensione di ogni pezzo è autocontratta, e un pezzo finale di dimensioni zero svolge il compito di segnare la fine della risposta.

Compressione

Quando si utilizza la risorsa compressa con gzip, il comportamento è in linea con quanto appena esplorato. In modalità di trasferimento identity, è comunque fondamentale fornire informazioni sulla lunghezza del contenuto, indipendentemente dalla codifica del contenuto.

Screenshot della scheda Rete che mostra ciò che è descritto di seguito.

Ora, parliamo dei benefici della compressione e di un compromesso. Scegliere la compressione gzip offre due vantaggi:

  1. conserva lo spazio su disco nel tuo server.
  2. riduce l’utilizzo della larghezza di banda.

Tuttavia, c’è un problema - il browser deve rimboccarsi le maniche e fare un po’ più di fatica per decomprimere.


Se sei interessato alle Prestazioni Web, devi sicuramente sapere di Web Caching (serie di post) 🔗.