Na navigaci | Klávesové zkratky

Translate to English… Ins Deutsche übersetzen…

Jak správně načítat webové fonty

Stránka by měla být čitelná ihned. Je velmi protivné, když si například v metru nemůžete přečíst článek jen kvůli tomu, že se nestihl načíst webový font.

Prohlížeče se totiž chovají tak, že text zobrazí až poté, co se font stáhne, aby zabránili tzv. Flash of Unstyled Text (FOUT), tedy nepříjemnému probliknutí jiného fontu. Problém je, že některé browsery nemají žádný timeout, po jehož uplynutí by se použil systémový font namísto žádného (tj. webového, ale nestaženého).

O tomhle tématu jsem psal už dříve a doporučoval pro mobilní zařízení webové fonty vůbec nepoužívat. Stejně krom autora grafiky to nikdy nepozná :-)

Mobilům (nebo lépe řečeno zařízením do šířky 500px) můžeme ulevit tímto způsobem:

/* font stáhneme jen na větších zařízeních */
@import "https://fonts.googleapis.com/css?family=PT+Serif" screen and (min-width: 500px);

/* systémové písmo */
body {
    font: 18px/1.7 Georgia, serif;
}

@media (min-width: 500px) {
    body { /* webový font použijeme jen na větších zařízeních */
        font-family: 'PT Serif', Georgia, serif;
    }
}

Ale co ostatní prohlížeče? Natahování fontu skrze @import vypadá elegantně, ale blokuje zpracování CSS a to zase blokuje vykreslení stránky. Jde tedy o nejhorší možné řešení. Jak se zbavit blokování a zajistit timeout?

Emulace timeoutu

Timeout lze emulovat pomocí JavaScriptu. Jenže zjistit, že se font načetl, není nic triviálního, dělá se to pomocí triků, jako je třeba detekce změny šířky předpřipraveného text atd. Font Loading API zatím podporuje jen Chrome. Takže lepší bude použít hotové řešení, jako je například Web Font Loader.

Web Font Loader

Knihovnu Web Font Loader vyvíjí Google společně s Typekit. Nedávno o ní psal Aleš Roubíček, takže na něj navážu a pokusím se upravit řešení tak, aby se skript načítal asynchronně a neblokoval stránku.

Do hlavičky stránky (!) vložte tento kód, kterým asynchronně načtete Web Font Loader a také font:

<script>
    WebFontConfig = {
        google: { families: ['PT+Serif:400:latin,latin-ext'] }
    };
</script>
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js" async defer></script>

Web Font Loader umí detekovat stavy, kdy se font načítá a kdy už je načtený, a to pomocí událostí nebo nastavováním tříd elementu <html>. Jakmile je font načtený, nastaví třídu wf-active. Zároveň řeší timeout.

Upravíme styl tak, aby se webový font použil až ve chvíli, kdy bude načtený, a nahradil tak systémové písmo.

/* systémové písmo */
body {
    font: 18px/1.7 Georgia, serif;
}

/* písmo po načtení fontu */
html.wf-active body {
    font-family: 'PT Serif', Georgia, serif;
}

Tohle řešení má ale potíž. Bude docházet k FOUT, tedy k probliknutí systémového fontu. Při prvním načtení stránky, kdy webový font ještě není v cache prohlížeče, může probliknutí trvat sekundu či déle, při každém dalším zobrazení krátký okamžik. A to vypadá velmi ošklivě.

Proto během načítání písmo skryjeme, tak jak to dělají prohlížeče standardně, bez loaderu.

html.wf-loading * {
    color: transparent !important;
}

Dále třídu wf-loading nastavíme ihned, nelze čekat, až se Web Font Loader načte. Ale zároveň ji musíme odstranit, když načtení loaderu selže. Výsledný kód vypadá takto:

<script>
    WebFontConfig = {
        google: { families: ['PT+Serif:400:latin,latin-ext'] }
    };

    var el = document.documentElement;
    el.className += ' wf-loading';
    setTimeout(function() {
        el.className = el.className.replace(/(^|\s)wf-loading(\s|$)/g, ' ');
    }, 1000); // 1 second
</script>
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js" async defer></script>

Pokud je font v cache, stránka se ihned zobrazí se správným písmem bez probliknutí. Pokud v cache není, text nejprve nebude vidět a pokud se do vteřiny nepodaří font stáhnout, zobrazí se systémovým písmem a po stažení se přepne na webové písmo. Tedy při rychlém stažení k probliknutí vůbec nedojde a při pomalém ano, ale uživatel nebude koukat na prázdnou stránku.

Samozřejmě časovou prodlevu můžete dle libosti snížit.

Doplnění: s alternativním řešením přišel Petr Soukup.

Po načtení fontu si do prohlížeče uloží cookie wfont, která později bude značit, že font by měl být už v cache prohlížeče:

<html>
<script>
    WebFontConfig = {
        google: { families: ['PT+Serif:400:latin,latin-ext'] },
        active: function() { document.cookie ='wfont=1; expires='+(new Date(new Date().getTime() + 86400000)).toGMTString()+'; path=/' }
    };
</script>

A při dalším načtení stránky, pokud tato cookie existuje, generuje trošku jiný kód, kde není Web Font Loader, ale:

<html class="wf-active">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=PT+Serif">

Tedy webový font se použije rovnou a načítá se bez JavaScriptu, nedochází tedy k žádnému probliknutí.

A tip nakonec: aby prohlížeč mohl započít načítání fontu hodně brzy, vlastně ještě před normálním zpracováváním stránky, mu lze poradit pomocí prefetch:

<link rel="prefetch" href="https://fonts.googleapis.com/css?family=PT+Serif">

Implementace v Latte by mohla vypadat třeba takto:

{var $wfont = isset($_COOKIE[wfont])}
<html n:class="$wfont ? wf-active">
<script n:syntax=off n:if=!$wfont>
WebFontConfig = {
    google: { families: ['PT+Serif:400:latin,latin-ext'] },
    active: function() { document.cookie ='wfont=1; expires='+(new Date(new Date().getTime() + 86400000)).toGMTString()+'; path=/' }
};
</script>
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js" async defer n:if=!$wfont></script>
<link rel="{$wfont ? stylesheet : prefetch}" href="https://fonts.googleapis.com/css?family=PT+Serif">

Komentáře

  1. Aleš Roubíček http://www.rarous.net/ #1

    avatar

    Lepší než zase texty schovávat je se je pokusit nastylovat (použít letter-spacing a line-height…) tak, aby se rozměry nenastylovaného textu blížily co nejvíce textu s vybraným fontem. Více viz skvělá kniha Responsive typography

    před rokem | odpovědět | reagoval [2] David Grudl
  2. David Grudl http://davidgrudl.com #2

    avatar

    #1 Aleši Roubíčku, Při asynchronním načítání problikne FOUT i když je vše v cache. Jde o setiny sekundy a lépe vypadá, když k tomu nedojde, tj. text je v ten okamžik skrytý.

    Kompromisem by mohlo být text skrýt maximálně na půl sekundy a pokud se font nenačte, použít alternativní (vhodně nastylovaný, i když je to cross OS ošidné) a teprve po načtení webový font.

    Příklad jsem upravil, aby wf-loading zrušil nejpozději po sekundě.

    před rokem | odpovědět | reagoval [4] Petr Soukup
  3. Petr Soukup https://www.souki.cz #3

    avatar

    Pozor na to, že @import zablokuje celý zbytek CSS, dokud se nestáhne externí. Nelze to proto používat ve stejném souboru se styly pro zbytek webu. Z nabízených možností na Google Webfonts je @import ta nejhorší.

    U Google Webfonts je navíc problém, že nejsou nijak extra cachované (vtipně to hlásí jako chybu i Google PageSpeed), takže je výhodnější hostovat je lokálně.

    Při používání wf-active dojde k probliknutí jen při prvním načtení. Zvlášť na mobilu je daleko lepší bliknutí nechat, než text skrývat. Pomocí CSS lze docílit toho, že jsou písma relativně podobná a probliknutí není až tak výrazné.

    před rokem | odpovědět | reagoval [5] David Grudl
  4. Petr Soukup https://www.souki.cz #4

    avatar

    #2 Davide Grudle, Při dalších načteních lze probliknutí zrušit. Děláme to tak, že při prvním načtení načítáme přes Web Font Loader a při dalším načtení už bez něj.
    Tak písmo při dalších načteních blokuje vykreslení, ale protože už je v cache, tak to nevadí.

    před rokem | odpovědět
  5. David Grudl http://davidgrudl.com #5

    avatar

    #3 Petře Soukupe, ad @import: jo, to je dobrá poznámka, doplním ji do článku.

    Můžeš vaše řešení popsat přesněji, nejlépe kódem?

    před rokem | odpovědět | reagoval [6] Petr Soukup
  6. Petr Soukup https://www.souki.cz #6

    avatar

    #5 Davide Grudle, Asi o tom konečně sepíšu článek. Ve zkratce to ale funguje tak, že se po načtení nastaví cookie a pokud existuje, tak se webfontloader vkládá bez async.
    Není to spolehlivé na 100%, ale nejhorší co se může stát je, že to třeba problikne při chybějící cookie. V principu je to podobné, jako tohle: https://www.souki.cz/…nchronni-css

    V praxi je to vidět třeba tady – https://www.jungle-gym.cz/. Koukám ale, že tam je teď z nějakého důvodu vypnuté cachovací proxy na fonty.

    před rokem | odpovědět | reagoval [7] David Grudl
  7. David Grudl http://davidgrudl.com #7

    avatar

    #6 Petře Soukupe, jo, to zní jako hodně dobrý způsob!

    před rokem | odpovědět
  8. Jakub Vrána http://php.vrana.cz/ #8

    avatar

    Při vyhazování CSS třídy vyhazuješ jednu mezeru navíc. V moderních prohlížečích se dá použít classList, ve starších tam tu mezeru prostě dej. Ještě ti chybí za tím příkazem středník.

    před rokem | odpovědět | reagoval [9] David Grudl
  9. David Grudl http://davidgrudl.com #9

    avatar

    #8 Jakube Vráno, díky, opraveno

    před rokem | odpovědět
  10. Honza Prachař #10

    avatar

    Ad ukládání do cookie, řeší to tak taky smashingmag, ale problém prý je, že HTTP keš na fonty není dostatečně spolehlivá. Viz odstavec o fontech http://www.smashingmagazine.com/…-case-study/

    před rokem | odpovědět | reagoval [11] Petr Soukup
  11. Petr Soukup https://www.souki.cz #11

    avatar

    #10 Honzo Prachaři, To je pravda, ale v tomhle případě je to celkem jedno. Pokud cookie nebo cache z nějakého důvodu selže, tak jediné co se stane je, že jedno načtení bude s bliknutím (nebo s blokovaným vykreslením).
    Je to ale vyjímečná situace, takže nic hrozného – navíc když drtivá většina webů to tak má při každém načtení.

    před rokem | odpovědět
  12. Petr Soukup https://www.souki.cz #12

    avatar

    Trvalo to pouhé 4 měsíce a konečně jsem pořádně sepsal, jak to řešíme u nás: https://www.souki.cz/…tat-webfonty
    Načítání bez blokování, bez skrývání písma a nic nebliká.

    před rokem | odpovědět

Zanechat komentář

Text komentáře
Kontakt

(kvůli gravataru)



*kurzíva* **tučné** "odkaz":http://example.com /--php phpkod(); \--