j'ai trouvé une police de 20 ko, et j'ai livré celle de 2 mo quand même

dev · Oct 4, 2026 · 4 min read

mon site déclare une plage de police U+0000-00FF — le latin de base, les 256 premiers points de code. en pratique cela veut dire des accents, de la ponctuation, et pas grand-chose d'autre. et pourtant le fichier derrière était de 1.945.520 octets.

dans le même dossier il y avait un second fichier, `GoogleSansFlex-Latin.woff2`, pesant 20.076 octets. même typeface. même dessinateur. 1% de la taille.

ce n'est pas une erreur d'arrondi. ce fichier faisait 0,99% de la taille de celui qui était téléchargé, donc 99% du poids de ma police de texte étaient là, inutilisés.

## la partie facile

changer le nom de fichier dans la règle `@font-face` est une modification de dix secondes. la page fonctionne toujours, le texte s'affiche toujours, et si vous ne la regardez plus jamais vous avez économisé 1,9 mo de bande passante pour chaque visiteur, pour toujours, sur chaque page.

c'est le moment où une personne raisonnable s'arrête et livre.

## alors j'ai mesuré d'abord

avant de toucher à quoi que ce soit, j'ai écrit une petite sonde de glyphes et je l'ai lancée dans un vrai navigateur, parce que la différence de taille est évidente mais la question utile est de savoir si le petit fichier couvre bien les caractères que le site utilise. on peut le faire sans aucune bibliothèque :

```
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 caractère que la police n'a pas tombe en repli, il se mesure donc comme
// la boîte notdef : comparez avec U+FFFF, auquel rien ne correspond
const base = width("Probe", "\uFFFF");
const missing = [...text].filter((ch) => width("Probe", ch) === base);
```

l'astuce, c'est la ligne de base `U+FFFF`. le glyphe notdef a une vraie largeur, et tout caractère que la police n'a pas s'y résout, donc une correspondance exacte avec cette largeur est votre test de glyphes manquants.

je l'ai lancé sur toute la plage qui m'intéresse — majuscules et minuscules, chiffres, ponctuation, et chaque lettre accentuée dont ce site a besoin pour avoir l'air correct en italien : `àèéìòùÀÈÉÌÒÙäöüßñç`, plus `€£¥©®™°·…`.

résultat : zéro glyphe manquant. le sous-ensemble de 20 ko couvrait tout, y compris des caractères bien en dehors de la plage déclarée.

ensuite j'ai vérifié la vraie chose au lieu de faire confiance à la sonde :

```
c.font = '50px "Google Sans Flex Local", monospace';
// 264,7 px — pas les 300 px que produit le repli, donc la vraie police est active
```

```
const rendered = width('"Google Sans Flex Local", monospace', "Handgloves");
const fallback = width("monospace", "Handgloves");
```

les deux diffèrent, donc le sous-ensemble était réellement actif et ne retombait pas en silence. par toutes les mesures à ma disposition, l'échange était correct.

## et puis je l'ai annulé

parce que les chiffres n'étaient pas tout le problème. deux choses que la mesure ne pouvait pas voir :

* **les formes ne sont pas identiques.** les deux fichiers sont découpés dans la même famille, mais les contours du sous-ensemble ne sont pas ceux de la police variable aux réglages d'axe par défaut. les titres de ma page sont le plus grand texte du site, et ils paraissaient visiblement plus petits et plus légers que ce sur quoi j'avais conçu.
* **les axes variables avaient disparu.** le fichier que j'ai livré est une police variable, et le site règle `wght` et `wdth` via `font-variation-settings`. un sous-ensemble découpé pour le latin ne porte pas nécessairement ces axes. perdre `wght` signifie que chaque `font-black` de la feuille de style retombe en silence sur le poids statique qui se trouve être.

une régression de taille de police sur le titre du hero, ce n'est pas quelque chose qu'un compteur d'octets va vous signaler. je l'ai vue, je ne l'ai pas aimée, et j'ai remis le fichier de 1,9 mo.

## ce que j'ai réellement gardé

le nombre qui a fait la différence n'est pas la taille. c'est le préchargement :

```
<link rel="preload" href="/fonts/GoogleSansFlex-Variable.woff2"
      as="font" type="font/woff2" crossorigin />
```

le site est d'abord du texte, donc cette police est sur le chemin critique. la précharger lance le téléchargement pendant l'analyse du html au lieu d'attendre que le css arrive et soit analysé, ce qui retire un aller-retour complet du premier rendu.

`crossorigin` est la partie que tout le monde oublie. les polices sont toujours récupérées en mode cors, même en same-origin, donc un preload sans cet attribut est jeté par le navigateur et le fichier est de toute façon récupéré une seconde fois. vous obtenez le pire des deux : le coût du preload et aucun bénéfice.

## la conclusion honnête

1,9 mo c'est trop pour une police de texte et je sais comment la descendre à 20 ko. la bonne correction n'est pas de choisir à la main un sous-ensemble latin — c'est de construire le sous-ensemble moi-même avec [`pyftsubset`](https://fonttools.readthedocs.io/en/latest/subset/index.html), en gardant les plages d'axes que la feuille de style utilise réellement, et de vérifier le résultat rendu par rapport à l'original avant de livrer.

tant que ça n'existe pas, le fichier de 2 mo reste, et le site est honnête là-dessus.

la partie utile de cet exercice n'était pas l'échange. c'était de remarquer que les deux fichiers étaient dans le même dossier depuis le début, et que je n'avais aucune idée de lequel le navigateur téléchargeait réellement. maintenant je vérifie `transferSize` au rechargement, qui vaut `0` pour chaque ressource que le navigateur a déjà. ce nombre est le seul rapport de performance honnête que j'aie jamais utilisé.