Než se stáhne vybrané písmo, prohlížeč vysází text náhradním. To má jinak široké znaky, takže se řádky zalomí jinde, bloky změní výšku a po doručení písma se všechno pod nimi posune. Čtenář ztratí místo v textu a hodnota Cumulative Layout Shift (CLS) roste.
Řešení je v CSS: náhradní písmo upravíte tak, aby zabíralo stejné místo jako to správné. Na detailu článku jednoho z našich webů to snížilo CLS z 0,152 na 0,025, s přednačtením řezu pak na 0. Za dobrou hodnotu se považuje nejvýš 0,1.
Stručný postup
- Nechte
font-display: swap. - Definujte náhradní písmo přes
@font-facesesize-adjusta přepsanými metrikami, zvlášť pro každý řez. - Přidejte samostatné rodiny pro Windows a Apple, Android a Linux.
- Všude nastavte číselnou výšku řádku.
- Přednačtěte jen jeden řez, ten pro běžný text.
- Měřte s prázdnou cache a se zpožděným písmem.
Hodnota font-display posun neodstraní
První nápad bývá změnit font-display. Takto vyšly jednotlivé hodnoty na šesti skutečných stránkách ve čtyřech šířkách, s písmem zpožděným o 1,5 sekundy, v Chrome a bez náhradního písma s upravenými metrikami. Čísla zahrnují jen posuny po doručení písma.
swap: průměrné CLS 0,007, nejvýš 0,042. Text je vidět hned, po doručení písma se přesází.block: průměrné CLS 0,007, nejvýš 0,041. Pomáhá jen zdánlivě. Neviditelný text se také sází podle náhradního písma, takže posun zůstává a čtenář navíc chvíli nevidí nic.optional: CLS 0. Písmo, které nedorazí včas, se při tomto zobrazení vůbec nepoužije, takže návštěvník na pomalém spojení uvidí celou stránku systémovým písmem.
optional je rozumná volba, pokud vám na vybraném písmu tolik nezáleží. Když se má použít vždy, zůstaňte u swap a upravte náhradní písmo. Na stejných stránkách pak CLS kleslo na nejvýš 0,001.
Náhradní písmo se shodnými metrikami
Náhradní písmo je běžný @font-face, který místo souboru odkazuje přes local() na písmo v systému. Popisovače ho přizpůsobí tomu vybranému:
size-adjustzvětší nebo zmenší písmo tak, aby sedla šířka textu.ascent-overrideadescent-overridenastaví prostor nad a pod účaří.line-gap-overridenastaví mezeru mezi řádky, obvykle na 0.
Příklad pro písmo Barlow a Arial v běžném a tučném řezu:
@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%;
}
Rodinu pak zařaďte hned za vybrané písmo:
body {
font-family: "Barlow", "Barlow Fallback", system-ui, sans-serif;
}
Jak hodnoty spočítat
size-adjust je poměr šířek stejného textu. Měřte na textu v jazyce webu, protože vzorek s diakritikou vychází jinak než anglický. Na stránce, která vybrané písmo načítá, spusťte v konzoli:
const sample = 'Příliš žluťoučký kůň úpěl ďábelské ódy. Sem vložte běžný text z webu.';
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);
Pro tučný řez změňte 400 na váhu vybraného písma a Arial měřte s váhou 700.
Metriky vybraného písma přečtete balíčkem fontTools:
from fontTools.ttLib import TTFont
font = TTFont('barlow-400.woff2')
print(font['head'].unitsPerEm, font['hhea'].ascent, font['hhea'].descent, font['hhea'].lineGap)
Barlow vrátí 1000 1000 -200 0, tedy výšku nad účařím 100 % a pod ním 20 % velikosti písma. size-adjust zvětšuje i přepsané metriky, proto je musíte vydělit stejným poměrem:
ascent-override = 100 % / 0,941 = 106,3 %
descent-override = 20 % / 0,941 = 21,3 %
Hodnoty umí spočítat také nástroje Capsize nebo Fontaine. Výsledek si ale ověřte na českém textu.
Každý řez a každý systém zvlášť
Tady většinou zůstává zbytek posunu, který se na první pohled nedaří vysvětlit.
Tučný řez má jiný poměr. Arial vůči Barlow vyšel na 94,1 % u běžného a 90,3 % u tučného řezu. Pokud použijete jednu hodnotu, nadpisy se zalomí jinak. Pro tučný řez také uveďte local("Arial Bold"), jinak prohlížeč běžný řez jen uměle ztuční.
Střední řez se může zalomit jinde. Řez 500 je obvykle na tlačítkách a v menu, kde se text vejde do řady jen těsně. Když náhradní písmo použilo metriky běžného řezu, tlačítka se v jednom záhlaví po doručení písma zalomila s posunem 0,073. Pomohl vlastní blok s váhou 500 a hodnotou o pár desetin vyšší.
Android Arial nemá. Bez vlastní rodiny tam náhradní písmo nefunguje a text je o 10 až 15 % širší. Totéž platí pro Linux bez Liberation Sans, kde se používá DejaVu Sans. Na kontaktní stránce jednoho webu tak přilepená lišta při sazbě náhradním písmem narostla a CLS dosáhlo 0,214. Lighthouse v Dockeru může běžet právě bez Arialu, takže někdy hlásí posun, který na vlastním počítači nevidíte.
Přidejte proto další rodiny. Prohlížeč použije první, jejíž local() na zařízení najde:
@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() hledá přesný název písma. Na Linuxu s Liberation Sans v našem testu font-family: Arial fungovalo, protože systém název přesměruje, ale local("Arial") selhalo. Uveďte proto local("Liberation Sans") zvlášť.
Safari a výška řádku
size-adjust umí Safari od verze 17, ale ascent-override, descent-override a line-gap-override starší verze ignorují. Přes @supports to nezjistíte, protože popisovače @font-face tímto způsobem testovat nejde.
Samotný size-adjust přitom bez číselné výšky řádku uškodí. Následující hodnoty pocházejí z testovací textové stránky v Chrome, vždy z 30 načtení v šesti šířkách. Safari tu zastupuje Chrome s vynechanými popisovači, není to tedy měření přímo v Safari:
line-height: normal, bez úprav: průměrné CLS 0,028line-height: normal, jensize-adjust: 0,056line-height: normal, všechny popisovače: 0,019- číselná výška řádku, jen
size-adjust: 0,020 - číselná výška řádku, všechny popisovače: 0,019
Pokud má text číselnou výšku řádku, je výška bloku daná jí, ne metrikami písma. Rozdíl mezi samotným size-adjust a všemi popisovači pak skoro mizí. Nastavte proto line-height číslem na všem textu včetně tlačítek a nadpisů. Zděděná hodnota z body stačí, pokud ji nic nepřepíše na normal. Pozor na tlačítka a formulářové prvky, které mají normal často už ve výchozích stylech prohlížeče.
Přednačtěte jen jeden řez
Bez přednačtení se prohlížeč o písmu dozví až po stažení a zpracování stylů. Přednačtení mu dá náskok, takže písmo někdy stihne dorazit před prvním vykreslením a k záměně vůbec nedojde.
<link rel="preload" as="font" type="font/woff2" crossorigin href="/build/barlow-400.woff2">
crossoriginje povinné i na vlastní doméně. Prohlížeč stahuje písma v režimu CORS a bez tohoto atributu by přednačtený soubor zahodil a stáhl ho znovu.- Přednačtěte jen řez běžného textu. Čtyři přednačtené soubory nám na pomalém spojení braly pásmo stylům, které blokují vykreslení, a zdržovaly LCP. Nadpisy dorazí o chvíli později, ale se shodnými metrikami se skoro nepohnou.
- Přednačtení je doplněk, ne řešení. Když styly přišly za 300 ms a písmo za 200 ms, zůstala stránka bez posunu ve 24 z 30 načtení, bez přednačtení jen v 5. Když písmo trvalo 1,5 sekundy, nepomohlo skoro vůbec: průměrné CLS 0,022 proti 0,023.
Menší soubory dorazí dřív
Čím dřív písmo dorazí, tím kratší dobu čtenář vidí náhradní. Hostujte ho na vlastní doméně, protože externí služba přidává další spojení a předání IP adresy třetí straně.
Balíčky jako Fontsource dělí písmo na podmnožiny latin a latin-ext. Česká diakritika je až ve druhé, takže česká stránka stahuje na každý řez dva soubory. Můžete je spojit do jednoho:
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
U písma Barlow to udělalo ze dvou souborů o 21,7 a 13,9 kB jeden o 22,8 kB. V @font-face pak uveďte unicode-range odpovídající obsahu souboru.
Jak výsledek změřit
Na vlastním počítači posun snadno přehlédnete, protože písmo je v cache a máte rychlé spojení.
- Zablokujte písmo. V Chrome DevTools otevřete Network, klikněte pravým tlačítkem na soubor
.woff2a zvolte Block request URL. Stránka zůstane v náhradním písmu a uvidíte, jestli sedí zalomení a výšky. - Zkontrolujte kritická místa. Nejvíc napoví nadpisy, tlačítka v jedné řadě, navigace a přilepené lišty, a to na šířce telefonu i počítače.
- Změřte záměnu. Zapněte Disable cache a v panelu Performance sledujte Layout shifts. Pomalé spojení nastavte tak, aby písmo dorazilo až po prvním vykreslení.
- Otestujte i jiný systém. Android a Linux mají jiná systémová písma, takže výsledek z Windows nebo macOS o nich nic neříká.
- Ověřte data od návštěvníků. Lighthouse změří jedno načtení v jednom prostředí. Rozhodující je CLS skutečných návštěvníků v Chrome UX Report nebo Search Console, kde se vyhodnocuje 75. percentil.
Shodné metriky posun zmenší, ale nezaručí nulu. Průměrná šířka může sedět a konkrétní odstavec se přesto zalomí o řádek jinak. Na testovací stránce změnilo po záměně písma výšku za 30 načtení 81 bloků bez úprav a 32 s upravenými metrikami. Přesně proto kontrolujte výsledek na vlastních stránkách a nespoléhejte jen na výpočet.
Kontrolní seznam
font-display: swap, pokud se má vybrané písmo použít vždy- náhradní rodina se
size-adjust,ascent-override,descent-overridealine-gap-override - zvláštní hodnoty pro běžný, střední a tučný řez
- samostatné rodiny pro Arial, Roboto a DejaVu Sans
local()včetně názvu Liberation Sans a tučných řezů- číselná výška řádku na všem textu
- přednačtení jen řezu běžného textu, s atributem
crossorigin - písmo na vlastní doméně, české znaky v jednom souboru
- kontrola se zablokovaným písmem na šířce telefonu i počítače
- CLS skutečných návštěvníků v Chrome UX Report nebo Search Console