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:
- paginazione
- streaming
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 sutrue
, questo parametro aggiunge l’intestazioneContent-Length
alla risposta. -
transfer
: Questo parametro imposta l’intestazioneTransfer-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 sutrue
, il server recupera la versione compressa del file. In questo caso, ilContent-Length
(se impostato) riflette la dimensione della versione compressa.
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.
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’intestazioneContent-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.
Ora, parliamo dei benefici della compressione e di un compromesso. Scegliere la compressione gzip offre due vantaggi:
- conserva lo spazio su disco nel tuo server.
- 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) 🔗.