Solange die gewählte Schrift nicht geladen ist, setzt der Browser den Text in einer Ersatzschrift. Deren Zeichen sind anders breit, die Zeilen brechen also an anderen Stellen um, Blöcke ändern ihre Höhe, und sobald die Schrift eintrifft, verschiebt sich alles darunter. Der Leser verliert die Stelle im Text, und der Wert für Cumulative Layout Shift (CLS) steigt.
Die Lösung steckt im CSS: Sie passen die Ersatzschrift so an, dass sie genauso viel Platz einnimmt wie die richtige. Auf einer Artikelseite einer unserer Websites sank CLS dadurch von 0,152 auf 0,025 und mit dem Vorladen der Schrift auf 0. Als gut gilt ein Wert von höchstens 0,1.
Das Vorgehen in Kürze
- Behalten Sie
font-display: swapbei. - Definieren Sie die Ersatzschrift über
@font-facemitsize-adjustund überschriebenen Metriken, für jeden Schriftschnitt einzeln. - Ergänzen Sie eigene Familien für Windows und Apple, Android und Linux.
- Setzen Sie überall eine numerische Zeilenhöhe.
- Laden Sie nur einen Schnitt vor, den für den Fließtext.
- Messen Sie mit leerem Cache und verzögerter Schrift.
Der Wert von font-display beseitigt die Verschiebung nicht
Der erste Gedanke ist meist, font-display zu ändern. Das sind die Ergebnisse auf sechs echten Seiten in vier Breiten, mit um 1,5 Sekunden verzögerter Schrift, in Chrome und ohne Ersatzschrift mit angepassten Metriken. Die Zahlen umfassen nur Verschiebungen nach dem Eintreffen der Schrift.
swap: durchschnittlicher CLS 0,007, höchstens 0,042. Der Text ist sofort sichtbar und wird neu gesetzt, sobald die Schrift eintrifft.block: durchschnittlicher CLS 0,007, höchstens 0,041. Das hilft nur scheinbar. Auch unsichtbarer Text wird nach der Ersatzschrift gesetzt, die Verschiebung bleibt also, und der Leser sieht zusätzlich eine Weile nichts.optional: CLS 0. Eine Schrift, die nicht rechtzeitig eintrifft, wird bei diesem Seitenaufruf gar nicht verwendet. Besucher mit langsamer Verbindung sehen also die ganze Seite in einer Systemschrift.
optional ist eine vernünftige Wahl, wenn Ihnen die gewählte Schrift nicht so wichtig ist. Soll sie immer verwendet werden, bleiben Sie bei swap und passen die Ersatzschrift an. Auf denselben Seiten sank CLS dann auf höchstens 0,001.
Eine Ersatzschrift mit passenden Metriken
Die Ersatzschrift ist ein gewöhnliches @font-face, das statt auf eine Datei über local() auf eine Systemschrift verweist. Deskriptoren passen sie an die gewählte Schrift an:
size-adjustskaliert die Schrift so, dass die Textbreite passt.ascent-overrideunddescent-overridelegen den Raum über und unter der Grundlinie fest.line-gap-overridelegt den Abstand zwischen den Zeilen fest, meist auf 0.
Ein Beispiel für die Schrift Barlow und Arial, in normalem und fettem Schnitt:
@font-face {
font-family: "Barlow Fallback";
font-weight: 100 400;
src: local("Arial"), local("Helvetica Neue"), local("Liberation Sans");
size-adjust: 94.1%;
ascent-override: 106.3%;
descent-override: 21.3%;
line-gap-override: 0%;
}
@font-face {
font-family: "Barlow Fallback";
font-weight: 600 900;
src: local("Arial Bold"), local("Arial-BoldMT"), local("Liberation Sans Bold");
size-adjust: 90.3%;
ascent-override: 110.7%;
descent-override: 22.1%;
line-gap-override: 0%;
}
Die Familie gehört dann direkt hinter die gewählte Schrift:
body {
font-family: "Barlow", "Barlow Fallback", system-ui, sans-serif;
}
So berechnen Sie die Werte
size-adjust ist das Verhältnis der Breiten desselben Textes. Messen Sie an Text in der Sprache Ihrer Website, denn eine Probe mit Umlauten oder anderen diakritischen Zeichen fällt anders aus als eine englische. Führen Sie auf einer Seite, die die gewählte Schrift lädt, in der Konsole Folgendes aus:
const sample = 'Zwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich. Fügen Sie hier normalen Text Ihrer Website ein.';
const context = document.createElement('canvas').getContext('2d');
const width = (font) => {
context.font = font;
return context.measureText(sample).width;
};
await document.fonts.load('400 100px Barlow', sample);
console.log(width('400 100px Barlow') / width('400 100px Arial') * 100);
Für den fetten Schnitt ändern Sie 400 auf das Gewicht der gewählten Schrift und messen Arial mit dem Gewicht 700.
Die Metriken der gewählten Schrift lesen Sie mit dem Paket fontTools aus:
from fontTools.ttLib import TTFont
font = TTFont('barlow-400.woff2')
print(font['head'].unitsPerEm, font['hhea'].ascent, font['hhea'].descent, font['hhea'].lineGap)
Barlow liefert 1000 1000 -200 0, also 100 % der Schriftgröße über der Grundlinie und 20 % darunter. size-adjust skaliert auch die überschriebenen Metriken, deshalb müssen Sie diese durch dasselbe Verhältnis teilen:
ascent-override = 100 % / 0,941 = 106,3 %
descent-override = 20 % / 0,941 = 21,3 %
Diese Werte berechnen auch die Werkzeuge Capsize und Fontaine. Prüfen Sie das Ergebnis aber an Text in Ihrer Sprache.
Jeder Schnitt und jedes System einzeln
Hier steckt meist die restliche Verschiebung, die sich auf den ersten Blick nicht erklären lässt.
Der fette Schnitt hat ein anderes Verhältnis. Arial gegenüber Barlow ergab 94,1 % beim normalen und 90,3 % beim fetten Schnitt. Mit nur einem Wert brechen Überschriften anders um. Geben Sie für den fetten Schnitt außerdem local("Arial Bold") an, sonst fettet der Browser den normalen Schnitt nur künstlich.
Der mittlere Schnitt kann an anderer Stelle umbrechen. Der Schnitt 500 steht meist auf Schaltflächen und in Menüs, wo der Text nur knapp in eine Reihe passt. Als die Ersatzschrift die normalen Metriken verwendete, brachen Schaltflächen in einem Seitenkopf nach dem Eintreffen der Schrift um, mit einer Verschiebung von 0,073. Geholfen hat ein eigener Block für das Gewicht 500 mit einem um einige Zehntel höheren Wert.
Android hat kein Arial. Ohne eigene Familie funktioniert die Ersatzschrift dort nicht, und der Text ist um 10 bis 15 % breiter. Dasselbe gilt für Linux ohne Liberation Sans, wo DejaVu Sans verwendet wird. Auf der Kontaktseite einer Website wuchs so eine fixierte Leiste, solange sie in der Ersatzschrift gesetzt war, und CLS erreichte 0,214. Lighthouse läuft in Docker unter Umständen ohne Arial und meldet daher manchmal eine Verschiebung, die Sie auf dem eigenen Rechner nicht sehen.
Ergänzen Sie deshalb weitere Familien. Der Browser verwendet die erste, deren local() er auf dem Gerät findet:
@font-face {
font-family: "Barlow Fallback Roboto";
font-weight: 100 400;
src: local("Roboto"), local("Roboto-Regular");
size-adjust: 95.9%;
ascent-override: 104.3%;
descent-override: 20.9%;
line-gap-override: 0%;
}
@font-face {
font-family: "Barlow Fallback DejaVu";
font-weight: 100 400;
src: local("DejaVu Sans"), local("DejaVuSans");
size-adjust: 83.9%;
ascent-override: 119.2%;
descent-override: 23.8%;
line-gap-override: 0%;
}
body {
font-family: "Barlow", "Barlow Fallback", "Barlow Fallback Roboto", "Barlow Fallback DejaVu", system-ui, sans-serif;
}
local() sucht nach dem exakten Schriftnamen. In unserem Test unter Linux mit Liberation Sans funktionierte font-family: Arial, weil das System den Namen umleitet, local("Arial") schlug jedoch fehl. Geben Sie local("Liberation Sans") deshalb separat an.
Safari und die Zeilenhöhe
Safari unterstützt size-adjust seit Version 17, ältere Versionen ignorieren aber ascent-override, descent-override und line-gap-override. Über @supports lässt sich das nicht feststellen, denn @font-face-Deskriptoren kann man so nicht testen.
Allein schadet size-adjust ohne numerische Zeilenhöhe sogar. Die folgenden Werte stammen von einer Testseite mit Text in Chrome, jeweils aus 30 Aufrufen in sechs Breiten. Safari wird hier durch Chrome ohne die übrigen Deskriptoren nachgestellt, es ist also keine Messung in Safari selbst:
line-height: normal, ohne Anpassung: durchschnittlicher CLS 0,028line-height: normal, nursize-adjust: 0,056line-height: normal, alle Deskriptoren: 0,019- numerische Zeilenhöhe, nur
size-adjust: 0,020 - numerische Zeilenhöhe, alle Deskriptoren: 0,019
Hat der Text eine numerische Zeilenhöhe, bestimmt diese die Höhe des Blocks und nicht die Schriftmetriken. Der Unterschied zwischen size-adjust allein und allen Deskriptoren verschwindet dann fast. Setzen Sie line-height deshalb als Zahl auf allen Text, auch auf Schaltflächen und Überschriften. Ein von body geerbter Wert genügt, solange ihn nichts mit normal überschreibt. Achten Sie auf Schaltflächen und Formularelemente, die normal oft schon in den Standardstilen des Browsers haben.
Nur einen Schnitt vorladen
Ohne Preload erfährt der Browser erst nach dem Laden und Verarbeiten der Stile von der Schrift. Ein Preload verschafft ihm einen Vorsprung, sodass die Schrift manchmal noch vor dem ersten Rendern eintrifft und gar kein Wechsel stattfindet.
<link rel="preload" as="font" type="font/woff2" crossorigin href="/build/barlow-400.woff2">
crossoriginist auch auf der eigenen Domain Pflicht. Browser laden Schriften im CORS-Modus, und ohne dieses Attribut würde die vorgeladene Datei verworfen und erneut geladen.- Laden Sie nur den Schnitt für den Fließtext vor. Vier vorgeladene Dateien nahmen unseren rendering-blockierenden Stilen bei langsamer Verbindung Bandbreite weg und verzögerten LCP. Überschriften treffen einen Moment später ein, bewegen sich mit passenden Metriken aber kaum.
- Ein Preload ist eine Ergänzung, keine Lösung. Kamen die Stile nach 300 ms und die Schrift nach 200 ms, blieb die Seite in 24 von 30 Aufrufen ohne Verschiebung, ohne Preload nur in 5. Dauerte die Schrift 1,5 Sekunden, half er kaum: durchschnittlicher CLS 0,022 gegenüber 0,023.
Kleinere Dateien treffen früher ein
Je früher die Schrift eintrifft, desto kürzer sieht der Leser die Ersatzschrift. Hosten Sie sie auf der eigenen Domain, denn ein externer Dienst bedeutet eine weitere Verbindung und die Weitergabe von IP-Adressen an Dritte.
Pakete wie Fontsource teilen eine Schrift in die Teilmengen latin und latin-ext auf. Diakritische Zeichen von Sprachen wie Tschechisch stehen erst in der zweiten, eine tschechische Seite lädt also pro Schnitt zwei Dateien. Sie lassen sich zu einer zusammenführen:
pip install fonttools brotli
fonttools ttLib.woff2 decompress -o latin.ttf barlow-latin-400-normal.woff2
fonttools ttLib.woff2 decompress -o ext.ttf barlow-latin-ext-400-normal.woff2
fonttools merge --output-file=merged.ttf latin.ttf ext.ttf
fonttools subset merged.ttf --unicodes="U+0000-017F,U+2000-206F,U+20AC,U+2122" --layout-features='*' --flavor=woff2 --output-file=barlow-400.woff2
Bei Barlow wurde aus zwei Dateien mit 21,7 und 13,9 kB eine mit 22,8 kB. Geben Sie in @font-face dann einen unicode-range an, der dem Inhalt der Datei entspricht.
So messen Sie das Ergebnis
Auf dem eigenen Rechner übersieht man die Verschiebung leicht, denn die Schrift liegt im Cache und die Verbindung ist schnell.
- Blockieren Sie die Schrift. Öffnen Sie in den Chrome DevTools den Bereich Network, klicken Sie mit der rechten Maustaste auf die
.woff2-Datei und wählen Sie Block request URL. Die Seite bleibt in der Ersatzschrift, und Sie sehen, ob Umbrüche und Höhen passen. - Prüfen Sie die kritischen Stellen. Am aussagekräftigsten sind Überschriften, Schaltflächen in einer Reihe, Navigation und fixierte Leisten, und zwar in Telefon- und Desktopbreite.
- Messen Sie den Wechsel. Aktivieren Sie Disable cache und beobachten Sie Layout shifts im Bereich Performance. Stellen Sie eine langsame Verbindung ein, damit die Schrift erst nach dem ersten Rendern eintrifft.
- Testen Sie auch ein anderes System. Android und Linux haben andere Systemschriften, ein Ergebnis aus Windows oder macOS sagt über sie also nichts aus.
- Prüfen Sie Daten von Besuchern. Lighthouse misst einen Aufruf in einer Umgebung. Entscheidend ist der CLS echter Besucher im Chrome UX Report oder in der Search Console, die das 75. Perzentil auswerten.
Passende Metriken verringern die Verschiebung, garantieren aber keine Null. Die durchschnittliche Breite kann stimmen, und ein bestimmter Absatz bricht trotzdem um eine Zeile anders um. Auf der Testseite änderten in 30 Aufrufen nach dem Wechsel ohne Anpassung 81 Blöcke die Höhe, mit angepassten Metriken 32. Prüfen Sie das Ergebnis deshalb auf Ihren eigenen Seiten und verlassen Sie sich nicht allein auf die Berechnung.
Checkliste
font-display: swap, wenn die gewählte Schrift immer verwendet werden soll- eine Ersatzfamilie mit
size-adjust,ascent-override,descent-overrideundline-gap-override - eigene Werte für den normalen, mittleren und fetten Schnitt
- eigene Familien für Arial, Roboto und DejaVu Sans
local()einschließlich des Namens Liberation Sans und der fetten Schnitte- eine numerische Zeilenhöhe auf allem Text
- Preload nur für den Schnitt des Fließtextes, mit dem Attribut
crossorigin - die Schrift auf der eigenen Domain, die Zeichen Ihrer Sprache in einer Datei
- eine Prüfung mit blockierter Schrift in Telefon- und Desktopbreite
- der CLS echter Besucher im Chrome UX Report oder in der Search Console