ho trovato un font da 20 kb e ho spedito comunque quello da 2 mb
dev · Oct 4, 2026 · 4 min read
il mio sito dichiara un intervallo di caratteri U+0000-00FF — latino base, i primi 256 punti di codice. in pratica significa accenti, punteggiatura e poco altro. eppure il file dietro pesava 1.945.520 byte.
nella stessa cartella c'era un secondo file, GoogleSansFlex-Latin.woff2, di 20.076 byte. stesso carattere tipografico, stesso designer. l'1% della dimensione.
non è un errore di arrotondamento. quel file era lo 0,99% della dimensione di quello che veniva scaricato, quindi il 99% del peso del mio font di testo stava lì inutilizzato.
la parte facile
cambiare il nome del file nella regola @font-face è una modifica di dieci secondi. la pagina funziona ancora, il testo si renderizza ancora, e se non ci torni mai sopra hai risparmiato 1,9 mb di banda a ogni visitatore, per sempre, su ogni pagina.
è il punto in cui una persona ragionevole si ferma e spedisce.
quindi ho misurato prima
prima di toccare qualcosa ho scritto una piccola sonda di glifi e l'ho eseguita in un browser vero, perché la differenza di dimensione è ovvia ma la domanda utile è se il file piccolo copre davvero i caratteri che il sito usa. si può fare senza nessuna libreria:
const load = (url) => new Promise((resolve) => {
const f = new FontFace("Probe", `url(${url})`, { weight: "100 1000" });
f.load().then(() => resolve("ok")).catch((e) => resolve("fail " + e.message));
document.fonts.add(f);
});
await load("/fonts/GoogleSansFlex-Latin.woff2");
await document.fonts.ready;
const c = document.createElement("canvas").getContext("2d");
const width = (family, ch) => {
c.font = `50px "${family}"`;
return c.measureText(ch).width;
};
// un carattere che il font non ha ricade sul fallback, quindi misura come
// il box notdef: confronta con U+FFFF, a cui non mappa nulla
const base = width("Probe", "\uFFFF");
const missing = [...text].filter((ch) => width("Probe", ch) === base);
il trucco è la base U+FFFF. il glifo notdef ha una larghezza reale, e qualsiasi carattere il font non ha si risolve a essa, quindi una corrispondenza esatta con quella larghezza è il tuo test per i glifi mancanti.
l'ho eseguito su tutto l'intervallo che mi interessa — maiuscole e minuscole, cifre, punteggiatura e ogni lettera accentata che questo sito deve mostrare correttamente in italiano: àèéìòùÀÈÉÌÒÙäöüßñç, più €£¥©®™°·….
risultato: zero glifi mancanti. il sottoinsieme da 20 kb copriva tutto, compresi caratteri ben fuori dall'intervallo dichiarato.
poi ho controllato la cosa vera invece di fidarmi della sonda:
c.font = '50px "Google Sans Flex Local", monospace';
// 264,7 px — non i 300 px che produce il fallback, quindi il font vero è attivo
const rendered = width('"Google Sans Flex Local", monospace', "Handgloves");
const fallback = width("monospace", "Handgloves");
i due differiscono, quindi il sottoinsieme era genuinamente attivo e non ricadeva in silenzio. secondo ogni misura a mia disposizione, lo scambio era corretto.
e poi l'ho annullato
perché i numeri non erano tutto il problema. due cose che la misurazione non poteva vedere:
- le forme non sono identiche. entrambi i file sono tagliati dalla stessa famiglia, ma i contorni del sottoinsieme non sono quelli del font variabile alle impostazioni predefinite degli assi. i titoli della mia pagina sono il testo più grande del sito, ed erano visibilmente più piccoli e più leggeri di quelli su cui avevo disegnato.
- gli assi variabili erano spariti. il file che ho spedito è un font variabile, e il sito imposta
wghtewdthconfont-variation-settings. un sottoinsieme tagliato per il latino non porta necessariamente quegli assi. perderewghtsignifica che ognifont-blacknel foglio di stile ricade in silenzio sul peso statico che capita.
una regressione di dimensione sul titolo dell'hero non è una cosa che un contatore di byte ti segnali. l'ho vista, non mi è piaciuta, e ho rimesso il file da 1,9 mb.
cosa ho davvero tenuto
il numero che ha fatto la differenza non è la dimensione. è il preload:
<link rel="preload" href="/fonts/GoogleSansFlex-Variable.woff2"
as="font" type="font/woff2" crossorigin />
il sito è basato sul testo, quindi quel font è sul percorso critico. precaricarlo avvia il download durante il parsing dell'html invece di aspettare che il css arrivi e venga parsato, e questo toglie un giro completo dal primo paint.
crossorigin è la parte che si dimentica. i font vengono sempre scaricati in modalità cors, anche same-origin, quindi un preload senza quell'attributo viene scartato dal browser e il file viene scaricato comunque una seconda volta. si ottiene il peggio di entrambi: il costo del preload e nessun beneficio.
la conclusione onesta
1,9 mb è troppo per un font di testo e so come ridurlo a 20 kb. la correzione giusta non è scegliere a mano un sottoinsieme latino — è costruire il sottoinsieme io con pyftsubset, mantenendo gli intervalli di assi che il foglio di stile usa davvero, e controllando il risultato renderizzato contro l'originale prima di spedire.
finché questo non esiste, il file da 2 mb resta e il sito è onesto al riguardo.
la parte utile di questo esercizio non è stato lo scambio. è stato notare che i due file stavano nella stessa cartella per tutto il tempo, e che non sapevo quale stesse davvero scaricando il browser. ora controllo transferSize a un reload, che legge 0 per ogni asset che il browser ha già in cache. quel numero è l'unico rapporto di performance onesto che abbia mai usato.