Wie man Schriftarten so lädt, dass FOUT vermieden wird und Lighthouse zufrieden ist

Avatar of Adrian Bece
Adrian Bece am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Ein Workflow für Webfonts ist einfach, oder? Wähle ein paar nett aussehende, webtaugliche Schriftarten, besorge dir den HTML- oder CSS-Code-Schnipsel, füge ihn ins Projekt ein und prüfe, ob sie richtig angezeigt werden. Leute machen das mit Google Fonts milliardenfach am Tag und fügen dessen <link>-Tag in den <head> ein.

Schauen wir uns an, was Lighthouse zu diesem Workflow zu sagen hat.

Stylesheets im <head> wurden von Lighthouse als render-blockierende Ressourcen markiert, und sie verursachen eine Verzögerung von einer Sekunde beim Rendern? Nicht gut.

Wir haben alles nach Lehrbuch, Dokumentation und HTML-Standards gemacht, also warum sagt uns Lighthouse, dass alles falsch ist?

Lassen Sie uns darüber sprechen, wie man Schriftarten-Stylesheets als render-blockierende Ressource eliminiert, und einen optimalen Aufbau durchgehen, der nicht nur Lighthouse zufriedenstellt, sondern auch den gefürchteten Flash of unstyled text (FOUT) überwindet, der normalerweise beim Laden von Schriftarten auftritt. Wir machen das alles mit reinem HTML, CSS und JavaScript, damit es auf jeden Tech-Stack angewendet werden kann. Als Bonus betrachten wir auch eine Gatsby-Implementierung sowie ein Plugin, das ich als einfache Drop-in-Lösung entwickelt habe.

Was wir unter „render-blockierenden“ Schriftarten verstehen

Wenn der Browser eine Website lädt, erstellt er einen Render-Baum aus dem DOM, d. h. einem Objektmodell für HTML, und CSSOM, d. h. einer Karte aller CSS-Selektoren. Ein Render-Baum ist Teil eines kritischen Renderpfads, der die Schritte darstellt, die der Browser durchläuft, um eine Seite zu rendern. Damit der Browser eine Seite rendern kann, muss er das HTML-Dokument und jede CSS-Datei laden und parsen, die in diesem HTML verlinkt ist.

Hier ist ein ziemlich typisches Schriftarten-Stylesheet, direkt von Google Fonts bezogen

@font-face {
  font-family: 'Merriweather';
  src: local('Merriweather'), url(https://fonts.gstatic.com/...) format('woff2');
}

Sie denken vielleicht, dass Schriftarten-Stylesheets wegen ihrer geringen Dateigröße unbedeutend sind, da sie normalerweise höchstens ein paar @font-face-Definitionen enthalten. Sie sollten keinen spürbaren Einfluss auf das Rendern haben, oder?

Nehmen wir an, wir laden eine CSS-Schriftartendatei von einem externen CDN. Wenn unsere Website lädt, muss der Browser warten, bis diese Datei vom CDN geladen und in den Render-Baum aufgenommen wird. Nicht nur das, er muss auch auf die Schriftartendatei warten, die als URL-Wert in der CSS @font-face-Definition referenziert wird, um angefordert und geladen zu werden.

Fazit: Die Schriftartendatei wird Teil des kritischen Renderpfads und erhöht die Verzögerung beim Rendern der Seite.

Verzögerung des kritischen Renderpfads beim Laden des Schriftarten-Stylesheets und der Schriftartendatei 
(Quelle: web.dev unter Creative Commons Attribution 4.0 Lizenz)

Was ist der wichtigste Teil einer Website für den durchschnittlichen Nutzer? Der Inhalt, natürlich. Deshalb muss der Inhalt dem Nutzer so schnell wie möglich im Ladeverfahren einer Website angezeigt werden. Um dies zu erreichen, muss der kritische Renderpfad auf kritische Ressourcen (z. B. HTML und kritisches CSS) reduziert werden, wobei alles andere nach dem Rendern der Seite geladen wird, einschließlich der Schriftarten.

Wenn ein Nutzer eine nicht optimierte Website über eine langsame, unzuverlässige Verbindung besucht, wird er verärgert sein, auf einem leeren Bildschirm zu sitzen und auf das Laden von Schriftartendateien und anderen kritischen Ressourcen zu warten. Das Ergebnis? Sofern dieser Nutzer nicht übermäßig geduldig ist, wird er wahrscheinlich einfach aufgeben und das Fenster schließen, in der Annahme, dass die Seite gar nicht lädt.

Wenn jedoch nicht-kritische Ressourcen aufgeschoben werden und der Inhalt so schnell wie möglich angezeigt wird, kann der Nutzer die Website durchsuchen und fehlende präsentationsspezifische Stile (wie Schriftarten) ignorieren – vorausgesetzt, sie behindern den Inhalt nicht.

Optimierte Websites rendern Inhalte mit kritischem CSS so schnell wie möglich, wobei nicht-kritische Ressourcen aufgeschoben werden. Ein Schriftartenwechsel tritt zwischen 0,5 s und 1,0 s in der zweiten Zeitleiste auf, was den Zeitpunkt anzeigt, zu dem die präsentationsspezifischen Stile mit dem Rendern beginnen.

Der optimale Weg zum Laden von Schriftarten

Es hat keinen Sinn, das Rad neu zu erfinden. Harry Roberts hat bereits gute Arbeit geleistet, indem er einen optimalen Weg zum Laden von Webfonts beschreibt. Er geht sehr detailliert mit gründlicher Recherche und Daten von Google Fonts vor und fasst alles in einem Vier-Schritte-Prozess zusammen.

  • Preconnect zum Ursprung der Schriftartendatei.
  • Preload des Schriftarten-Stylesheets asynchron mit geringer Priorität.
  • Asynchrones Laden des Schriftarten-Stylesheets und der Schriftartendatei nach dem Rendern des Inhalts mit JavaScript.
  • Bereitstellen einer Fallback-Schriftart für Benutzer mit deaktiviertem JavaScript.

Lassen Sie uns unsere Schriftart nach Harrys Ansatz implementieren.

<!-- https://fonts.gstatic.com is the font file origin -->
<!-- It may not have the same origin as the CSS file (https://fonts.googleapis.com) -->
<link rel="preconnect"
      href="https://fonts.gstatic.com"
      crossorigin />

<!-- We use the full link to the CSS file in the rest of the tags -->
<link rel="preload"
      as="style"
      href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" />

<link rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap"
      media="print" onload="this.media='all'" />

<noscript>
  <link rel="stylesheet"
        href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" />
</noscript>

Beachten Sie das media="print" im Link zum Schriftarten-Stylesheet. Browser geben Druck-Stylesheets automatisch eine niedrige Priorität und schließen sie vom kritischen Renderpfad aus. Nachdem das Druck-Stylesheet geladen wurde, wird ein onload-Ereignis ausgelöst, das Medium wird auf einen Standardwert all geändert und die Schriftart wird auf alle Medientypen (Bildschirm, Druck und Sprache) angewendet.

Lighthouse ist mit diesem Ansatz zufrieden!

Es ist wichtig zu beachten, dass das Selbst-Hosting von Schriftarten auch helfen kann, render-blockierende Probleme zu beheben, aber das ist nicht immer möglich. Die Verwendung eines CDN zum Beispiel kann unvermeidlich sein. In einigen Fällen ist es vorteilhaft, die Hauptlast beim Ausliefern statischer Ressourcen einem CDN zu überlassen.

Obwohl wir nun das Schriftarten-Stylesheet und die Schriftartendateien auf dem optimalen, nicht-render-blockierenden Weg laden, haben wir ein kleines UX-Problem eingeführt...

Flash of unstyled text (FOUT)

Das nennen wir FOUT.

Warum passiert das? Um eine render-blockierende Ressource zu eliminieren, müssen wir sie laden, nachdem der Seiteninhalt gerendert (d. h. auf dem Bildschirm angezeigt) wurde. Im Fall eines niedrig priorisierten Schriftarten-Stylesheets, das asynchron nach kritischen Ressourcen geladen wird, kann der Nutzer den Moment sehen, in dem sich die Schriftart von der Fallback-Schriftart zur heruntergeladenen Schriftart ändert. Nicht nur das, das Layout der Seite kann sich verschieben, was dazu führt, dass einige Elemente kaputt aussehen, bis die Webfont geladen ist.

Der beste Weg, mit FOUT umzugehen, ist, den Übergang zwischen der Fallback-Schriftart und der Webfont reibungslos zu gestalten. Um das zu erreichen, müssen wir

  • Eine geeignete Fallback-Systemschriftart auswählen, die der asynchron geladenen Schriftart so genau wie möglich entspricht.
  • Die Schriftartenstile anpassen (font-size, line-height, letter-spacing usw.) der Fallback-Schriftart, um die Eigenschaften der asynchron geladenen Schriftart wieder so genau wie möglich anzupassen.
  • Die Stile für die Fallback-Schriftart löschen, sobald die asynchron geladene Schriftartendatei gerendert wurde, und die für die neu geladene Schriftart vorgesehenen Stile anwenden.

Wir können Font Style Matcher verwenden, um optimale Fallback-Systemschriftarten zu finden und diese für jede geplante Webfont zu konfigurieren. Sobald wir Stile sowohl für die Fallback- als auch für die Webfont bereit haben, können wir mit dem nächsten Schritt fortfahren.

Merriweather ist die Schriftart und Georgia ist die Fallback-Systemschriftart in diesem Beispiel. Sobald die Merriweather-Stile angewendet sind, sollte es minimale Layoutverschiebungen geben und der Wechsel zwischen den Schriftarten sollte weniger auffällig sein.

Wir können die CSS Font Loading API verwenden, um zu erkennen, wann unsere Webfont geladen ist. Warum das? Typekit's Web Font Loader war einst eine der beliebtesten Methoden dafür, und obwohl es verlockend ist, ihn oder ähnliche Bibliotheken weiterhin zu verwenden, müssen wir Folgendes berücksichtigen:

  • Er wurde seit über vier Jahren nicht mehr aktualisiert, was bedeutet, dass, wenn auf der Plugin-Seite etwas fehlschlägt oder neue Funktionen benötigt werden, es wahrscheinlich niemand implementieren und warten wird.
  • Wir handhaben das asynchrone Laden bereits effizient mit Harry Roberts' Snippet und wir müssen uns nicht auf JavaScript verlassen, um die Schriftart zu laden.

Wenn Sie mich fragen, ist die Verwendung einer Typekit-ähnlichen Bibliothek einfach zu viel JavaScript für eine so einfache Aufgabe. Ich möchte keine Drittanbieter-Bibliotheken und Abhängigkeiten verwenden, also implementieren wir die Lösung selbst und versuchen, sie so einfach und unkompliziert wie möglich zu gestalten, ohne sie zu überkomplizieren.

Obwohl die CSS Font Loading API als experimentelle Technologie gilt, hat sie eine Browserunterstützung von etwa 95 %. Aber unabhängig davon sollten wir eine Fallback-Lösung bereitstellen, falls die API in Zukunft geändert oder als veraltet eingestuft wird. Das Risiko, eine Schriftart zu verlieren, ist die Mühe nicht wert.

Die CSS Font Loading API kann zum dynamischen und asynchronen Laden von Schriftarten verwendet werden. Wir haben bereits entschieden, uns nicht auf JavaScript für etwas so Einfaches wie das Laden von Schriftarten zu verlassen, und wir haben es auf optimale Weise mit reinem HTML mit Preload und Preconnect gelöst. Wir werden eine einzelne Funktion aus der API verwenden, die uns hilft zu prüfen, ob die Schriftart geladen und verfügbar ist.

document.fonts.check("12px 'Merriweather'");

Die Funktion check() gibt true oder false zurück, je nachdem, ob die in den Funktionsargumenten angegebene Schriftart verfügbar ist oder nicht. Der Wert des Parameters für die Schriftgröße ist für unseren Anwendungsfall nicht wichtig und kann auf jeden Wert gesetzt werden. Dennoch müssen wir sicherstellen, dass

  • Wir mindestens ein HTML-Element auf einer Seite haben, das mindestens ein Zeichen mit einer deklarierten Webfont enthält. In den Beispielen verwenden wir &nbsp;, aber jedes Zeichen kann verwendet werden, solange es (ohne display: none;) sowohl für sehende als auch für nicht-sehende Benutzer verborgen ist. Die API verfolgt DOM-Elemente, auf die Schriftartenstile angewendet werden. Wenn keine passenden Elemente auf einer Seite vorhanden sind, kann die API nicht feststellen, ob die Schriftart geladen wurde oder nicht.
  • Die in den Argumenten der Funktion check() angegebene Schriftart ist genau das, wie die Schriftart im CSS genannt wird.

Ich habe den Listener für das Laden von Schriftarten mit der CSS Font Loading API in der folgenden Demo implementiert. Zu Demonstrationszwecken werden das Laden von Schriftarten und der Listener dafür durch Klicken auf die Schaltfläche ausgelöst, um einen Seitenlade-Vorgang zu simulieren, damit Sie die Änderung sehen können. In regulären Projekten sollte dies bald nach dem Laden und Rendern der Website geschehen.

Ist das nicht großartig? Wir haben weniger als 30 Zeilen JavaScript benötigt, um einen einfachen Listener für das Laden von Schriftarten zu implementieren, dank einer gut unterstützten Funktion aus der CSS Font Loading API. Wir haben auch zwei mögliche Randfälle im Prozess behandelt:

  • Etwas geht mit der API schief oder es tritt ein Fehler auf, der das Laden der Webfont verhindert.
  • Der Benutzer besucht die Website mit deaktiviertem JavaScript.

Jetzt, da wir eine Möglichkeit haben zu erkennen, wann die Schriftartendatei fertig geladen ist, müssen wir unserer Fallback-Schriftart Stile hinzufügen, um die Webfont anzupassen, und sehen, wie FOUT effektiver gehandhabt wird.

Der Übergang zwischen der Fallback-Schriftart und der Webfont sieht reibungslos aus und wir haben es geschafft, einen wesentlich weniger auffälligen FOUT zu erzielen! Auf einer komplexen Website würde diese Änderung zu weniger Layout-Shifts führen, und Elemente, die von der Inhaltsgröße abhängen, würden nicht kaputt oder deplatziert aussehen.

Was unter der Haube passiert

Werfen wir einen genaueren Blick auf den Code aus dem vorherigen Beispiel, beginnend mit dem HTML. Wir haben den Schnipsel im <head>-Element, der es uns ermöglicht, die Schriftart asynchron mit Preload, Preconnect und Fallback zu laden.

<body class="no-js">
  <!-- ... Website content ... -->
  <div aria-visibility="hidden" class="hidden" style="font-family: '[web-font-name]'">
      /* There is a non-breaking space here */
  </div>
  <script> 
    document.getElementsByTagName("body")[0].classList.remove("no-js");
  </script>
</body>

Beachten Sie, dass wir eine fest codierte Klasse .no-js im <body>-Element haben, die entfernt wird, sobald das HTML-Dokument vollständig geladen ist. Dies wendet Webfont-Stile für Benutzer mit deaktiviertem JavaScript an.

Zweitens, erinnern Sie sich, wie die CSS Font Loading API mindestens ein HTML-Element mit einem einzelnen Zeichen benötigt, um die Schriftart zu verfolgen und ihre Stile anzuwenden? Wir haben ein <div> mit einem &nbsp;-Zeichen hinzugefügt, das wir auf zugängliche Weise sowohl für sehende als auch für nicht-sehende Benutzer verstecken, da wir display: none; nicht verwenden können. Dieses Element hat einen inline font-family: 'Merriweather'-Stil. Dies ermöglicht es uns, reibungslos zwischen den Fallback-Stilen und den geladenen Schriftartenstilen zu wechseln und sicherzustellen, dass alle Schriftartendateien ordnungsgemäß verfolgt werden, unabhängig davon, ob sie auf der Seite verwendet werden oder nicht.

Beachten Sie, dass das &nbsp;-Zeichen nicht im Code-Schnipsel angezeigt wird, aber es ist da!

Der CSS ist der einfachste Teil. Wir können die CSS-Klassen nutzen, die im HTML fest codiert sind oder bedingt mit JavaScript angewendet werden, um verschiedene Zustände des Schriftartenladens zu handhaben.

body:not(.wf-merriweather--loaded):not(.no-js) {
  font-family: [fallback-system-font];
  /* Fallback font styles */
}


.wf-merriweather--loaded,
.no-js {
  font-family: "[web-font-name]";
  /* Webfont styles */
}


/* Accessible hiding */
.hidden {
  position: absolute; 
  overflow: hidden; 
  clip: rect(0 0 0 0); 
  height: 1px;
  width: 1px; 
  margin: -1px;
  padding: 0;
  border: 0; 
}

JavaScript ist die Magie. Wie bereits beschrieben, prüfen wir, ob die Schriftart geladen wurde, indem wir die check()-Funktion der CSS Font Loading API verwenden. Auch hier kann der Parameter für die Schriftgröße jeder Wert (in Pixeln) sein; der Wert der Schriftfamilie muss dem Namen der Schriftart entsprechen, die wir laden.

var interval = null;


function fontLoadListener() {
  var hasLoaded = false;


  try {
    hasLoaded = document.fonts.check('12px "[web-font-name]"')
  } catch(error) {
    console.info("CSS font loading API error", error);
    fontLoadedSuccess();
    return;
  }
  
  if(hasLoaded) {
    fontLoadedSuccess();
  }
}


function fontLoadedSuccess() {
  if(interval) {
    clearInterval(interval);
  }
  /* Apply class names */
}


interval = setInterval(fontLoadListener, 500);

Was hier passiert, ist, dass wir unseren Listener mit fontLoadListener() einrichten, der in regelmäßigen Abständen läuft. Diese Funktion sollte so einfach wie möglich sein, damit sie effizient innerhalb des Intervalls läuft. Wir verwenden den try-catch-Block, um Fehler zu behandeln und Probleme abzufangen, sodass Webfont-Stile auch im Falle eines JavaScript-Fehlers angewendet werden, damit der Benutzer keine UI-Probleme erfährt.

Als Nächstes berücksichtigen wir den Fall, dass die Schriftart erfolgreich geladen wird, mit fontLoadedSuccess(). Wir müssen sicherstellen, dass wir zuerst das Intervall löschen, damit die Überprüfung nicht unnötigerweise danach läuft.  Hier können wir Klassennamen hinzufügen, die wir benötigen, um die Webfont-Stile anzuwenden.

Und schließlich starten wir das Intervall. In diesem Beispiel haben wir es auf 500 ms eingestellt, sodass die Funktion zweimal pro Sekunde ausgeführt wird.

Hier ist eine Gatsby-Implementierung

Gatsby macht im Vergleich zur reinen Webentwicklung (und sogar zum regulären Create-React-App-Tech-Stack) ein paar Dinge anders, was die Implementierung des bisher Behandelten etwas knifflig macht.

Um dies zu vereinfachen, entwickeln wir ein lokales Gatsby-Plugin, sodass sich der gesamte Code, der für unseren Schriftartenlader relevant ist, im Beispiel unten unter plugins/gatsby-font-loader befindet.

Unser Schriftartenlader-Code und die Konfiguration werden auf die drei Haupt-Gatsby-Dateien aufgeteilt.

  • Plugin-Konfiguration (gatsby-config.js): Wir werden das lokale Plugin in unser Projekt aufnehmen, alle lokalen und externen Schriftarten und ihre Eigenschaften auflisten (einschließlich des Schriftartennamens und der CSS-Datei-URL) und alle Preconnect-URLs einfügen.
  • Server-seitiger Code (gatsby-ssr.js): Wir werden die Konfiguration verwenden, um Preload- und Preconnect-Tags im HTML-<head> mithilfe der Funktion setHeadComponents aus der Gatsby-API zu generieren und einzufügen. Dann generieren wir die HTML-Snippets, die die Schriftart verstecken, und fügen sie über setPostBodyComponents in HTML ein.
  • Client-seitiger Code (gatsby-browser.js): Da dieser Code ausgeführt wird, nachdem die Seite geladen wurde und nachdem React gestartet ist, ist er bereits asynchron. Das bedeutet, wir können die Schriftarten-Stylesheet-Links mit react-helmet einfügen. Wir starten auch einen Schriftarten-Lade-Listener, um FOUT zu behandeln.

Sie können die Gatsby-Implementierung im folgenden CodeSandbox-Beispiel ansehen.

Ich weiß, einige dieser Dinge sind komplex. Wenn Sie nur eine einfache Drop-in-Lösung für performantes, asynchrones Schriftartenladen und FOUT-Bekämpfung wünschen, habe ich ein gatsby-omni-font-loader-Plugin genau dafür entwickelt. Es verwendet den Code aus diesem Artikel und ich pflege es aktiv. Wenn Sie Vorschläge, Fehlerberichte oder Codebeiträge haben, können Sie diese gerne auf GitHub einreichen.

Fazit

Der Inhalt ist vielleicht die wichtigste Komponente für die Benutzererfahrung auf einer Website. Wir müssen sicherstellen, dass der Inhalt oberste Priorität hat und so schnell wie möglich geladen wird. Das bedeutet, dass während des Ladevorgangs minimale präsentationsspezifische Stile (d. h. inline kritisches CSS) verwendet werden. Deshalb werden Webfonts in den meisten Fällen als nicht-kritisch betrachtet – der Nutzer kann den Inhalt auch ohne sie konsumieren – daher ist es völlig in Ordnung, wenn sie nach dem Rendern der Seite geladen werden.

Das kann jedoch zu FOUT und Layout-Shifts führen, daher ist der Listener für das Laden von Schriftarten erforderlich, um einen reibungslosen Wechsel zwischen der Fallback-Systemschriftart und der Webfont zu ermöglichen.

Ich würde gerne Ihre Gedanken dazu hören! Lassen Sie mich in den Kommentaren wissen, wie Sie das Problem des Webfont-Ladens, render-blockierender Ressourcen und FOUT auf Ihren Projekten angehen.


Referenzen