Zaratan@next

Charger une font proprement

Cet article fait parti de la série sur ce blog en NextJS. L'introduction avec la liste des autres articles est disponible ici: Intro.

On va parler ici des fonts et de toutes les problématiques associées, comment on les résout.

La base: Comment utiliser une font en CSS

components/ArticleList.module.scss:28
* {
  font-family: Consolas, Monaco, monospace;
}

Le navigateur va utiliser la première font de la liste (de gauche a droite) qu'il possède. Les font qui ne sont pas disponible seront ignorées.

Les différents types de font

Il y a globalement 2 types de fonts. Celles qui sont déjà sur les ordinateurs des utilisateurs et celles qu'il faut aller chercher. Ces dernières ont plusieurs "saveurs" en fonction de là où elles viennent exactement.

Font "de base"

Chaque OS possède un sous-ensemble de font pré-installées. Ces fonts n'ont donc pas de temps de chargement et seront donc affichées instantanément.

Malheureusement les fonts disponibles sur chaque OS diffèrent grandement et il faut donc souvent choisir des équivalents par OS.

Par exemple, dans le code donné précédemment:

* {
  font-family: Consolas, Monaco, monospace;
}

Consolas est seulement disponible sur Windows, Monaco est seulement sous Mac et monospace est une famille de font qui sera utilisé dans les autres OS.

Vous pouvez trouver ici une liste des "safe-font" disponible par OS. Et sur wikipedia une liste a jour des fonts par OS: Mac et Windows

Font Online

Un certain nombre de font supplémentaires sont disponibles Online. Il faudra donc les importer séparément dans son CSS et définir une nouvelle famille de font (Nous verrons les différentes façons de le faire plus bas).

Il existe un certain nombre de format de font. Ce fut pendant longtemps un sujet complexe (Looking at you IE8). De nos jours, si vous ne devez pas supporter IE11 => Woff2 et si vous devez supporter IE11 => Woff et Woff2. C'est deux format ouverts et c'est supporté par quasiment tous les navigateurs utilisés (Toujours regarder le usage-relative dans caniuse.com). Woff2 étant un itération sur le format Woff qui offre une meilleure compression.

Il existe des dizaines de sites qui proposent des fonts (payantes ou non) et qui vont servir de CDN pour celles-ci. Par exemple Adobe et fonts.com.

Font Custom locale

De nombreux sites proposent de télécharger directement les polices pour ensuite pouvoir les servir depuis votre site web. C'est comme une version "Online" classique sauf que c'est vous qui hébergez votre font. Eventuellement ça va impliquer de devoir la servir depuis un CDN configuré par vos soins ou avec le reste de votre site web. Comme le chargement d'une font va forcement générer des désagréments pour votre utilisateur, vous devriez traiter votre font comme une image critique pour votre site.

Font Google

Google fonts héberge un grand nombre de polices gratuites avec leur propre CDN. Vous devriez traiter ça comme une font online normale mais on va voir qu'il y a quelques différences quant à la façon qu'a Google de faire charger les fonts.

Les façons de charger sa font

Avec un font-family

N'importe où dans votre CSS vous pouvez ajouter une nouvelle famille de font disponible pour votre navigateur de la façon suivante:

@font-face {
  font-family: "MyCustomFont";
  src: url("https://url.to.my.font.woff2") format("woff2"), url("https://url.to.my.font.woff")
      format("woff");
}

Et l'utiliser comme une font standard du système, par exemple:

components/ArticleList.module.scss:33
* {
  font-family: Inconsolata, Consolas, Monaco, monospace;
}

Ici, Inconsolata n'est pas une police de base et est téléchargée par le navigateur.

Que fait Google fonts ?

Si on suit une url de google fonts (https://fonts.googleapis.com/css2?family=Inconsolata par exemple) on voit que google sert un CSS qui contient des font-face pré-écrites qui couvriront le range entier de la font:

/* vietnamese */
@font-face {
  font-family: "Inconsolata";
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  src: url(https://fonts.gstatic.com/s/inconsolata/v20/QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lpp4U8WRL2kXWdycuJDETf.woff)
    format("woff");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169,
    U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: "Inconsolata";
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  src: url(https://fonts.gstatic.com/s/inconsolata/v20/QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lpp4U8WRP2kXWdycuJDETf.woff)
    format("woff");
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
    U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: "Inconsolata";
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  src: url(https://fonts.gstatic.com/s/inconsolata/v20/QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lpp4U8WR32kXWdycuJDA.woff)
    format("woff");
  unicode-range: 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;
}

Donc pour utiliser une font Google ce que nous faisons réellement c'est 2 sauts/appels. Un premier pour télécharger le CSS avec les @font-face et n avec les différentes src de font.

Google propose deux façons d'importer les fonts.

  • Avec un @import CSS
<style>
  @import url("https://fonts.googleapis.com/css2?family=Inconsolata&display=swap");
</style>
  • Dans le html
<link
  href="https://fonts.googleapis.com/css2?family=Inconsolata&display=swap"
  rel="stylesheet"
/>

Les deux façons sont globalement équivalentes si et seulement si le @import est bien utilisé dans le head de votre html et pas dans un fichier tiers CSS. Si le import est mis dans un fichier CSS on se retrouve à faire un saut de plus:

  1. Aller chercher le CSS => se rendre compte qu'il y a un import d'un tiers
  2. Aller chercher le tiers chez google => se rendre compte qu'il demande de charger des polices
  3. Aller chercher des polices.

Pendant ce temps, le reste de votre fichier CSS n'est pas appliqué à votre page web.

Dans le doute, n'utilisez pas le @import et préférez le <link>

Les problématiques

Tout ça étant dit, vous allez rencontrer 4 problématiques majeures avec l'utilisation d'une font qui n'est pas présente nativement.

Blocking CSS

Si vous utilisez une font custom, votre site ne sera pas considéré comme "chargé" avant que la font ne soit téléchargée et appliquée. Sur un ordinateur connecté a votre fibre ce n'est pas un problème mais c'est une toute autre histoire sur un mobile de 2010 connecté à du edge chancelant. La plupart du temps, la police peut être appliquée dans un second temps sans que ça gène l'utilisation du site.

Ce CSS doit il être bloquant ?

Déjà il faut se poser la question principale, est-ce que votre font est si importante qu'elle doit bloquer tout le chargement de votre site web ? Personnellement je ne vois que très très peu de cas où c'est éventuellement sujet à débat. Le cas d'un logo qui est un texte avec une font particulière par exemple me vient à l'esprit. Mais même dans ce cas ça ne serait pas un oui automatique.

La technique de média

Cette technique marche pour tout CSS qui sera "non-obligatoire" pour charger un site et pas seulement dans le cas d'un chargement de police.

Prenons un lien de chargement d'un fichier CSS:

<link
  href="https://fonts.googleapis.com/css2?family=Inconsolata"
  rel="stylesheet"
/>

Dès que la page va rencontrer ce lien elle va charger le CSS à l'URL indiqué et l'intégrer dans son premier rendu. Ceci est du au fait que le navigateur "sait quoi faire" du fichier.

Un des attributs supplémentaires qu'on peut passer à un link est: media Je cite MDN:

Cet attribut indique le média auquel s'applique la ressource liée. Sa valeur doit être une requête média. Cet attribut est principalement utilisé pour permettre à l'agent utilisateur de sélectionner la meilleure feuille de style en fonction de l'appareil de l'utilisateur.

Les valeurs valide d'une requête media sont: all, print, screen et speech.

Mais que se passe-t-il quand un media non-valide est spécifié ? Le fichier est chargé mais non exécuté (car le media ne correspond pas au media courant) et donc n'est pas bloquant.

<link
  href="https://fonts.googleapis.com/css2?family=Inconsolata"
  rel="stylesheet"
  media="nope"
/>

Vous me direz qu'on est bien avancé. C'est sûr qu'on a plus de CSS bloquant mais le fichier n'est pas chargé donc on a l'air malin. Vous auriez raison.

Cependant, il existe un autre type d'attribut qu'on peut utiliser: onload qui exécutera du JS quand le fichier sera chargé. JS qu'on va utiliser pour changer la valeur de media vers quelque-chose de valide.

<link
  href="https://fonts.googleapis.com/css2?family=Inconsolata"
  rel="stylesheet"
  media="nope"
  onload="if(media!='all')media='all'"
/>

On a donc maintenant un fichier CSS qui sera chargé dans le site de manière non-bloquante.

Le cas spécial de NextJS + Google

NextJS n'aime pas trop qu'on manipule la balise onload d'un link dans le <head>. Il existe un package npm pour charger des fonts Google utilisant cette technique de media: next-google-fonts.

Si on regarde dans le code source on peut trouver une variation de cette technique à la sauce React :

next-google-fonts/src/index.tsx:35
<link href={href} rel="stylesheet" media={!hydrated ? "print" : "all"} />

On utilise le package de cette façon dans NextJS:

pages/_app.tsx:25
<GoogleFonts href="https://fonts.googleapis.com/css2?family=Inconsolata&display=swap" />

La UI qui ne s'affiche pas avant que la font soit chargée

En fonction de comment vous déclarez votre @font-face le texte ayant cette police peut:

  • Soit ne pas s'afficher du tout tant que la police n'est pas chargée, ce qui peut faire un site inutilisable;
  • Soit changer quand la police est chargée ce qui peut faire un changement de police alors que l'utilisateur a commencé à lire votre article.

En tant que développeur on a donc le choix entre le FOUT (Flash of unstyled text) ou le FOIT (Flash of invisible text) le temps qu'une police soit chargé. Cet article en parle très bien et je vais essayer de résumer tout ça.

Les différents type de chargement de font

Pour choisir son type de chargement de font dans un @font-face on va utiliser la propriété font-display.

Elle a plusieurs valeurs possibles :

  • auto: La valeur par défaut. On laisse le navigateur décider. C'est a priori toujours un mauvais choix car on veut maitriser l'expérience du chargement de notre font. On peut avoir droit a un FOIT ou un FOUT avec un timeout long (30 secondes dans les vieux safari IOS) sans le savoir.
  • block: Une "courte" période de texte invisible. Suivit d'un fallback si la font n'est toujours pas chargée. Et dès que la font est chargée on l'affiche. La durée de la période dépend du navigateur et c'est en général 3 secondes => Non. Personne ne veut attendre 3 secondes pour éventuellement pouvoir lire son texte.
  • swap: Pas de période de texte invisible, la police de fallback est tout de suite affichée et éventuellement sera remplacée par la font finale quand elle aura chargé.
  • fallback: 100ms de texte invisible, puis fallback et dès que la font est chargé on échange pour celle-ci. A mon sens c'est un "meilleur" block.
  • optional: Le browser a 100ms de texte invisible pour charger la font sinon le fallback sera utilisé pour toujours.

Cet article en plus d'être très bien offre un graphisme qui permet de bien se représenter les différentes options qu'on a.

Si vous chargez votre font depuis Google fonts. Il y a un paramètre passable dans l'url qui fait que le CSS généré par Google aura le bon font display. Exemple: https://fonts.googleapis.com/css2?family=Inconsolata&display=swap.

À mon humble avis, vous voudrez quasiment toujours swap. Mais c'est toujours bien de savoir qu'il existe d'autres options.

Slow

Dans tous les cas, charger une font c'est "lent". La rapidité avec laquelle votre front va charger votre police va impacter l'expérience utilisateur. Que vous soyez FOUT ou FOIT, le moins de temps l'utilisateur passe avec une UI non-finale le mieux c'est.

Préchargement des fonts

Il existe une option pour dire a votre navigateur de charger aussi vite que possible un fichier CSS (on le marque comme critique): rel="preload"

Par exemple:

<link
  rel="preload"
  href="Inconsolata.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

Ça a comme défaut que si vous n'utilisez pas la technique de media vue plus haut. Ce chargement devient VRAIMENT bloquant pour votre page.

Le cas spécial de Google fonts

Comme on l'a vu Google fonts fait un saut et ne nous laisse pas vraiment charger les fonts comme on veut. Mais on peut tout de même utiliser cette technique avec quelques variations:

On commence par se préconnecter aussi tôt que possible dans notre html a fonts.gstatic.com ce qui réduit autant que possible le temps de négociation SSL pour les différentes polices qui seront chargées.

<link
  rel="preconnect"
  href="https://fonts.gstatic.com"
  crossorigin="anonymous"
/>

Puis on va preload le fichier CSS correspondant:

<link
  rel="preload"
  href="https://fonts.googleapis.com/css?family=Inconsolata&display=swap"
/>

Si vous utilisez la librairie next-google-fonts, ces étapes sont faites automatiquement:

next-google-fonts/src/index.tsx:29-34

Shift de la UI post chargement

Et finalement, si vous avez choisi d'opter pour le FOUT (ce qui a mon humble avis devrait être le choix par défaut), vous aller remarquer que la police "système" et la police finale font différentes tailles. Ça va impliquer qu'après le chargement de votre police finale le texte voir le layout de la page risque de bouger et perturber la lecture et/ou le rendu.

Overlapping fonts

La technique va être d'appliquer un CSS différent en fonction de si la police finale est chargée ou non.

Il existe pour ça un super outil: Overlap tool

Par défaut monospace et Inconsolata ne prennent pas du tout le même espace.

Après ajustement on peut les faire coïncider presque parfaitement.

Dans ce blog les deux ensemble de valeurs qu'on utilisera sont:

Sans Inconsolata:

components/ArticleList.module.scss:29-31
* {
  font-size: 16px;
  line-height: 1.25;
  letter-spacing: 0.39px;
}

Avec Inconsolata:

components/ArticleList.module.scss:34-36
* {
  font-size: 20px;
  line-height: 1;
  letter-spacing: 0;
}

Le switch d'un CSS a un autre sera effectué avec du JS. Soit avec des choses comme https://github.com/typekit/webfontloader.

Soit beaucoup plus simple en React en utilisant : fontfaceobserver.

Votre serviteur (moi) vous propose même un hook pour facilement empaqueter la logique:

hooks/useWatchFont.ts
import { useEffect, useState } from "react";
import FontFaceObserver from "fontfaceobserver";

const useWatchFont = (fontName: string) => {
  const [fontLoaded, setFontLoaded] = useState(false);
  useEffect(() => {
    const fontObserver = new FontFaceObserver(fontName);
    fontObserver.load().then(() => setFontLoaded(true));
  }, [fontName]);

  return fontLoaded;
};

export default useWatchFont;

Qu'on utilisera de la manière suivante:

components/ArticleList.tsx:15-30
const isInconsolataLoaded = useWatchFont("Inconsolata");
const inconsolataLoadedClass = isInconsolataLoaded ? "inconsolata-loaded" : "";
// …
<a className={inconsolataLoadedClass} />;
// …

et

components/ArticleList.module.scss:28-37
* {
  font-family: Consolas, Monaco, monospace;
  font-size: 16px;
  line-height: 1.25;
  letter-spacing: 0.39px;
  &:global(.inconsolata-loaded) {
    font-family: Inconsolata, Consolas, Monaco, monospace;
    font-size: 20px;
    line-height: 1;
    letter-spacing: 0;
  }
}

Conclusion

Ne négligez pas vos fonts c'est une façon facile de gagner en performance et en utilisabilité avec quelques techniques in-fine assez simples.

Pour aller plus loin quelques liens en vrac