Un média en ligne de 8 000 pages perd 12 points de CLS moyen après un redesign. La cause n'est ni une image mal dimensionnée, ni un banner publicitaire : c'est le passage de System UI à une police custom Inter chargée sans stratégie. Chaque page subit un flash de texte invisible (FOIT) suivi d'un reflow brutal quand la police arrive. Google le mesure, CrUX l'enregistre, et le trafic organique décroche de 9 % en six semaines.
Le chargement des polices est l'un des derniers angles morts de la performance web. Tout le monde optimise les images, personne ne regarde la font stack.
FOUT, FOIT et FOFT : comprendre ce qui se passe réellement
Trois comportements distincts se produisent quand un navigateur rencontre une @font-face non encore chargée. Chacun a des conséquences mesurables sur le CLS et le LCP.
FOIT — Flash of Invisible Text
Le navigateur masque le texte pendant le téléchargement de la police. Sur Chrome et Edge (moteur Blink), le texte reste invisible pendant un "block period" de 3 secondes avant un fallback. Sur Safari (WebKit), le block period est potentiellement infini : si la police ne charge jamais, le texte ne s'affiche jamais.
Impact SEO direct : si le LCP de votre page est un élément texte (titre H1, premier paragraphe), le FOIT retarde le LCP du temps de téléchargement de la police. Sur une connexion 3G lente, ajoutez 1,5 à 4 secondes au LCP. Google mesure le LCP sur le champ (CrUX), pas en lab — les utilisateurs mobiles en zone rurale font partie de votre P75.
FOUT — Flash of Unstyled Text
Le navigateur affiche immédiatement le texte en police fallback, puis swap vers la police custom une fois chargée. C'est le comportement de font-display: swap.
Le FOUT est meilleur pour le LCP (le texte est visible immédiatement) mais génère un reflow quand la police custom arrive. Si la fallback et la custom ont des métriques typographiques différentes (hauteur de x, chasse, interligne), le layout shift est brutal. Sur un article de blog avec 2000 mots, un swap Inter → Arial peut provoquer un CLS de 0.15+ — bien au-delà du seuil de 0.1 recommandé par Google.
FOFT — Flash of Faux Text
Technique avancée où vous chargez d'abord un subset de la police (le Regular) avec font-display: swap, puis les variantes (Bold, Italic) en différé. Le navigateur synthétise les graisses manquantes avec le faux bold/italic en attendant les fichiers réels. Deux swaps au lieu d'un, mais le premier est quasi imperceptible si la font Regular est préchargée.
Le FOFT n'est pertinent que si vous utilisez 3+ variantes d'une même famille. Pour un site qui n'utilise que Regular et Bold, le preload de la font principale + font-display: optional est plus simple et plus efficace.
font-display : chaque valeur a un trade-off
La propriété CSS font-display contrôle le comportement du navigateur pendant le chargement. Aucune valeur n'est universellement correcte — le choix dépend de votre architecture de contenu.
/* Cas 1 : média/blog — le texte EST le contenu principal */
@font-face {
font-family: 'Editorial';
src: url('/fonts/editorial-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: optional; /* Pas de swap, pas de CLS */
}
/* Cas 2 : e-commerce — le texte est secondaire, les images sont le LCP */
@font-face {
font-family: 'BrandFont';
src: url('/fonts/brand-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; /* Texte visible ASAP, CLS acceptable car LCP = image */
}
/* Cas 3 : SaaS dashboard — utilisateurs récurrents, cache warm */
@font-face {
font-family: 'UIFont';
src: url('/fonts/ui-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: fallback; /* Block court (100ms), swap court (3s), puis abandon */
}
swap : block period ~0ms, swap period infinie. Le texte s'affiche en fallback immédiatement, swap quand la font arrive. Bon pour le LCP, mauvais pour le CLS. C'est le choix par défaut de Google Fonts. C'est aussi le choix qui génère le plus de layout shifts sur les sites text-heavy.
optional : block period ~100ms, pas de swap period. Si la police n'est pas chargée en ~100ms, le navigateur utilise la fallback pour toute la durée de la page et met la police en cache pour la visite suivante. Zéro CLS garanti. Le trade-off : les first-time visitors ne voient pas votre custom font.
fallback : compromis entre swap et optional. Block period ~100ms, swap period ~3s. Si la font arrive dans les 3 premières secondes, swap. Sinon, fallback définitif.
Pour un site où le CLS est déjà un problème identifié, optional est la seule valeur qui élimine le layout shift lié aux fonts avec certitude. Pour tout le reste, c'est un calcul de risque : combien de CLS êtes-vous prêt à accepter pour garantir la custom font au first load ?
Preload, subsetting et self-hosting : la stack technique complète
Le font-display seul ne suffit pas. La vraie optimisation se joue en amont : réduire le temps de téléchargement de la police pour que le swap (ou le block period) devienne imperceptible.
Preload des polices critiques
Le <link rel="preload"> permet au navigateur de commencer le téléchargement de la police avant même d'avoir parsé le CSS. C'est le levier le plus puissant pour réduire le FOIT/FOUT.
<head>
<!-- Preload UNIQUEMENT les polices au-dessus de la ligne de flottaison -->
<link
rel="preload"
href="/fonts/editorial-regular-latin.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<!-- NE PAS preload les variantes secondaires -->
<!-- Le bold, l'italic, les graisses 300/500/700 se chargent en lazy -->
<!-- Si vous utilisez Google Fonts, preconnect au domaine -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
</head>
Règles strictes :
- Maximum 2 preloads font par page. Chaque preload consomme de la bande passante early-stage. Preloader 4-5 variantes de police revient à bloquer le téléchargement du CSS critique et des images LCP. Vérifiez dans Chrome DevTools > Network > Priority que vos preloads ne cannibalisent pas le LCP.
- L'attribut
crossoriginest obligatoire, même en self-hosting. Sans lui, le navigateur fait deux requêtes (une sans CORS, une avec) et le preload est gaspillé. - Seul le format WOFF2 mérite un preload. La compression Brotli intégrée de WOFF2 réduit la taille de 30% par rapport à WOFF. Le support navigateur est à 97%+ selon Can I Use.
Subsetting : ne chargez que ce que vous affichez
Une police Inter complète pèse ~300 KB en WOFF2 (toutes les graisses, tous les caractères Unicode). Le subset Latin de Inter Regular pèse ~18 KB. Le ratio est de 16:1.
# Installer pyftsubset (outil de la Google Font Tools)
pip install fonttools brotli
# Subset pour le latin + les caractères spéciaux français
pyftsubset Inter-Regular.ttf \
--output-file=Inter-Regular-latin.woff2 \
--flavor=woff2 \
--layout-features='kern,liga,calt,frac,tnum' \
--unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD"
# Résultat typique : 18-22 KB au lieu de 95 KB par variante
L'argument --layout-features est critique. Si vous conservez toutes les features OpenType (ligatures contextuelles, fractions, tabular numbers), le fichier grossit de 20-40%. Gardez kern et liga, supprimez le reste sauf besoin spécifique.
Pour un site multilingue, créez des subsets par plage Unicode et chargez-les avec unicode-range dans votre @font-face. Le navigateur ne télécharge que le subset nécessaire pour le contenu de la page :
/* Latin */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC;
font-display: optional;
}
/* Cyrillique — chargé uniquement si la page contient des caractères cyrilliques */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-cyrillic.woff2') format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
font-display: optional;
}
Self-hosting vs CDN : le calcul a changé
Google Fonts est pratique mais implique une connexion tierce (dns-lookup + TCP + TLS vers fonts.gstatic.com). Même avec preconnect, le coût est de 100-300ms en P75. En self-hosting, la police est servie depuis votre domaine — zéro connexion supplémentaire.
L'argument historique en faveur de Google Fonts ("la police est déjà dans le cache du navigateur grâce aux autres sites") ne tient plus. Chrome a partitionné le cache HTTP par site depuis la version 86 (octobre 2020). Un utilisateur qui a chargé Inter sur site-a.com la rechargera intégralement sur site-b.com.
En 2026, le self-hosting est le choix par défaut pour la performance. L'exception : les sites qui utilisent 10+ polices Google avec des subsets exotiques (CJK, Devanagari) où la complexité de maintenance du self-hosting ne vaut pas le gain.
Override des métriques de fallback : la technique qui tue le CLS
Le vrai CLS des fonts ne vient pas du swap lui-même — il vient de la différence de métriques entre la police fallback et la police custom. Si les deux ont la même hauteur de ligne, la même chasse moyenne et le même ascender/descender, le swap ne provoque aucun reflow visible.
CSS propose quatre descripteurs dans @font-face pour ajuster les métriques de la fallback : size-adjust, ascent-override, descent-override, et line-gap-override.
/* Fallback "ajustée" pour matcher Inter */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107.64%;
ascent-override: 90.49%;
descent-override: 22.56%;
line-gap-override: 0%;
}
/* Police custom */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin.woff2') format('woff2');
font-display: swap; /* swap SANS CLS grâce au fallback ajusté */
}
/* Usage */
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}
Avec ces overrides, le font-display: swap ne génère plus de layout shift mesurable. Vous obtenez le meilleur des deux mondes : texte visible immédiatement (bon LCP) et zéro CLS au swap.
Comment calculer ces valeurs ? Deux options :
- Fontaine — bibliothèque de l'écosystème Nuxt/UnJS qui génère automatiquement les overrides. Intégrable dans Vite, Webpack, Next.js.
- @next/font (maintenant
next/font) — Next.js calcule et injecte automatiquement les fallback overrides quand vous utilisez le module built-in. C'est la solution la plus transparente si vous êtes sur Next.
Pour les calculs manuels, l'outil Fallback Font Generator compare les métriques de votre custom font avec les system fonts et génère le CSS.
Scénario concret : migration d'un e-commerce de 15K pages
Un e-commerce mode sous Next.js (App Router, SSR) avec 15 000 pages produit et 3 000 pages catégories. Le site utilise trois polices custom : Montserrat (headings), Lato (body), et une icon font (navigation).
Avant optimisation
- Montserrat (6 variantes) + Lato (4 variantes) = 10 fichiers WOFF2, 380 KB total
- Chargés via Google Fonts CDN avec
font-display: swap - Aucun preload
- CLS P75 mobile : 0.21 (mesuré sur CrUX via Search Console)
- LCP P75 mobile : 3.8s (le LCP est souvent le titre H1 de la fiche produit, retardé par le font loading)
- 62% des pages produit sont en "needs improvement" sur CLS dans le rapport Core Web Vitals
Actions menées
1. Audit des variantes utilisées. Analyse du CSS en production avec Chrome DevTools > Network > filtre Font. Résultat : seules 4 variantes sur 10 sont réellement utilisées (Montserrat 600, Montserrat 700, Lato 400, Lato 700). Les 6 autres sont déclarées dans le CSS mais jamais appliquées à un élément visible. Suppression immédiate.
2. Migration en self-hosting + subsetting. Les 4 fichiers sont subsetés en Latin Extended (pyftsubset). Taille totale : 380 KB → 68 KB. Les fichiers sont servis depuis le même domaine avec des headers cache immutables :
# Nginx — cache immutable pour les fonts
location /fonts/ {
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary "Accept-Encoding";
# Brotli statique si disponible
brotli_static on;
gzip_static on;
}
3. Preload des 2 polices critiques (Montserrat 700 pour les titres, Lato 400 pour le body). Les variantes secondaires chargent naturellement via le CSS.
4. Fallback metric overrides générés avec next/font. Passage de font-display: swap (avec CLS) à font-display: swap + overrides (sans CLS mesurable).
5. Remplacement de l'icon font par des SVG inline. L'icon font pesait 45 KB et provoquait un FOIT sur les icônes de navigation (panier, menu, recherche). Les SVG inline sont rendus immédiatement par le parseur HTML — pas de FOIT, pas de requête réseau supplémentaire.
Résultats après 8 semaines (données CrUX)
- CLS P75 mobile : 0.21 → 0.04
- LCP P75 mobile : 3.8s → 2.6s (gain dû au preload + self-hosting + suppression du RTT vers Google Fonts)
- Pages "good" en CLS : 38% → 91%
- Trafic organique : +14% sur les pages produit (corrélation, pas causalité unique — d'autres optimisations d'images ont été faites en parallèle)
Le point clé : la suppression de 6 fichiers font inutilisés a eu plus d'impact que n'importe quel tweak CSS. L'audit "quelles polices sont réellement rendues" est la première étape de toute optimisation font.
Monitoring et détection des régressions font
Les régressions font sont insidieuses. Un développeur ajoute une variante font-weight: 500 dans un composant React. Le CSS inclut maintenant un @font-face supplémentaire. Personne ne remarque que le fichier WOFF2 correspondant n'a pas été subseté et pèse 95 KB. Le CLS P75 dégrade progressivement sur 3 semaines, noyé dans le bruit des données CrUX.
Ce qu'il faut monitorer
- Nombre de fichiers font chargés par page. Si ce nombre augmente sans décision explicite, c'est une régression. Chrome DevTools > Network > Type: Font donne la liste, mais uniquement en ponctuel.
- Taille totale des fonts par page. Seuil d'alerte raisonnable : >100 KB total pour un site text-heavy, >60 KB pour un e-commerce.
- Présence du
font-displaydans chaque@font-face. Un@font-facesansfont-displayhérite du comportement par défaut du navigateur (FOIT avec block period de 3s sur Blink). C'est le type de régression qu'un outil comme Seogard peut détecter automatiquement en crawlant le CSS de chaque page. - CLS field data par template. Ne regardez pas le CLS moyen du site. Segmentez par type de page (produit, catégorie, article, homepage). Une régression font sur un template affecte toutes les pages de ce template — potentiellement des milliers de pages d'un coup.
Automatiser la détection
Intégrez un check dans votre CI/CD. Lighthouse CI peut être configuré pour échouer le build si le nombre de font requests dépasse un seuil :
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: [
'http://localhost:3000/',
'http://localhost:3000/produit/exemple',
'http://localhost:3000/categorie/exemple',
],
},
assert: {
assertions: {
// Aucun @font-face sans font-display
'font-display': ['error', { minScore: 1 }],
// CLS inférieur à 0.1
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
// Nombre de requêtes third-party (inclut Google Fonts si non self-hosted)
'third-party-summary': ['warn', { maxLength: 0 }],
},
},
},
};
Ce check CI ne remplace pas le monitoring field. Les données lab (Lighthouse) ne captent pas les conditions réseau réelles. Mais il empêche les régressions évidentes d'atteindre la production.
Pour le monitoring continu en production, croisez les données CrUX via Search Console avec un crawl régulier du CSS. Si un crawl détecte un nouveau @font-face ou un font-display manquant, l'alerte doit partir avant que CrUX ne reflète la dégradation — CrUX a un rolling window de 28 jours, ce qui signifie qu'une régression font peut mettre 4 semaines à apparaître dans les données et 4 semaines supplémentaires à disparaître après correction.
Edge cases et trade-offs à connaître
font-display: optional et les revisites
Avec optional, les nouveaux visiteurs voient la police système. La custom font est téléchargée en arrière-plan et mise en cache. Au second chargement, la font est disponible en cache local et s'affiche en <100ms. Ce comportement est parfait pour les SaaS et les sites à fort taux de revisites. Il est problématique pour les sites médias où 60%+ du trafic est constitué de one-time visitors venant des SERPs — ces visiteurs ne verront jamais votre police custom.
Variable fonts : un seul fichier, toutes les graisses
Les variable fonts permettent de remplacer 4-6 fichiers statiques par un seul fichier qui contient toutes les variations d'épaisseur, de largeur et de style. Inter Variable pèse ~100 KB subseté en Latin — plus qu'un seul fichier statique (18 KB) mais moins que 4 fichiers statiques (72 KB). Le trade-off : un seul RTT au lieu de quatre, mais un fichier plus lourd. Si vous utilisez 3+ variantes de la même famille, la variable font est presque toujours gagnante en performance réseau.
Les icon fonts doivent mourir
Les icon fonts (FontAwesome, Material Icons) cumulent tous les problèmes : FOIT sur les icônes de navigation (l'utilisateur voit un rectangle vide pendant 3 secondes), pas de subsetting natif facile, et un fichier de 100-300 KB pour afficher 15 icônes. Remplacez-les par des SVG inline ou un sprite SVG. Le seul cas où une icon font reste acceptable : un back-office interne où la performance n'est pas un critère SEO.
SSR et polices inline
Si vous êtes sur un stack SSR ou ISR, le CSS des @font-face est souvent inlined dans le <head> par le framework. Vérifiez que le chemin de la police dans le CSS inliné correspond bien au chemin servi par votre CDN. Un mismatch entre le path relatif SSR et le path absolu CDN est une source classique de 404 sur les fonts — invisible dans le rendu visuel (le navigateur tombe silencieusement sur la fallback) mais mesurable en CLS et en gaspillage de requêtes réseau. L'article sur comment tester ce que Google voit réellement couvre les outils pour auditer ce type de divergence SSR/client.
La stratégie font optimale se résume en quatre décisions : self-host en WOFF2 subseté, preload 1-2 fichiers critiques, choisir le bon font-display pour votre type de contenu, et aligner les métriques de la fallback avec les overrides CSS. L'exécution prend une journée. Le maintien dans le temps prend un monitoring continu — un outil comme Seogard qui crawle votre CSS à intervalles réguliers et alerte sur les @font-face sans font-display ou les nouvelles polices non optimisées transforme une victoire ponctuelle en avantage permanent.