CSS rychle a efektivně

Taky pořád čtete, jak optimalizovat CSS tím, že je spojíte do jednoho velkého souboru, který načtete v hlavičce HTML stránky?

No, Google si teď uvědomil, že to není tak úplně nejlepší a snaží se Chrome (a tedy i Operu a Safari) přepsat tak, aby se – světe div se – choval stejně jako Internet Explorer. Vývojáři Microsoftu totiž do IE (ať už záměrně nebo nechtěně) zakomponovali mechanismus, který umožňuje načítat CSS efektivněji po částech.

V čem je háček?

Dopravní zácpa vs. CSS
Stahovat CSS jednotlivě je jako jet do práce auty

Představte si načítání CSS jako cestu do práce. Pokud všechny soubory nalinkujete v hlavičce, je to jako by všichni lidé sedli ráno v 7 hodin do aut a vyrazili do práce. Pak se samozřejmě všechny ulice ucpou nikdo se nikam nedostane dřív než po osmé (v případě HTML – dřív než se všechny soubory nestáhnou do prohlížeče).

Tohle řeší hromadná doprava, kdy se všichni lidé nacpou do malého autobusu (tramvaje, metra, vlaku, apod.) a jedou společně.

Hromadná doprava vs. CSS
Stahovat CSS najednou je jako jet do práce autobusem

Výhoda je v tom, že ulice nejsou tak ucpané, ale každý jednotlivý člověk musí čekat, až ostatní nastoupí  a vystoupí tam, kde zrovna bydlí nebo pracují. A navíc, pokud vám autobus ujede, musíte čekat několik (desítek) minut na další. Cesta je tedy celkově rychlejší, ale pořád ne tak rychlá a pohodlná, jako kdyby jel člověk sám prázdnou ulicí přesně od svého domu ke své práci.

A stejně je to i s hromadným načítáním CSS. Sice se všechna data stáhnout rychle, protože prohlížeč nemusí neustále otevírat nová spojení pro každý soubor, ale zase musí čekat, až se stáhnou všechny definice včetně těch, které zrovna nepotřebuje (např. pro skryté prvky jako je vysouvací menu, nebo pro komponenty, které na současné stránce ani nejsou). Druhou nevýhodou je, že pokud se změní jedna jediná definice, je potřeba znovu stáhnout celý soubor.

Při cestě do práce, pokud nutně potřebujete jet autem, platí jednoduché pravidlo: pokud jsou ulice ucpané mezi 7. a 8. hodinou, je potřeba vyrazit před půl sedmou nebo až po osmé.

A stejně to můžete udělat i s CSS soubory. Tedy v hlavičce načíst jen to nejnutnější a zbytek načítat až postupně společně s tím, jak se budou načítat jednotlivé části (komponenty) stránky. Když prohlížeč najde na stránce článek, stáhne si k němu styly pro zobrazení článku, když najde obrázek, stáhne styly pro uspořádání obrázků, atd. až nakonec najde menu a patičku na konci stránky a stáhne pro ně příslušné styly (a nebo také ne, protože ví, že nejsou vidět – to ale budete muset ošetřit přes JS).

Jak na komponentní CSS

Systém načítání spočívá v tom, že před tím, než do HTML vypíšete určitou část stránky, vložíte kód pro načtení stylu (link). Prohlížeč, když najde odkaz na styl, pozastaví vykreslování stránky a počká, dokud se soubor nestáhne a pak teprve pokračuje ve vykreslování:

<html><head>
    <link rel=stylesheet href="main.css">
    <!-- main.css obsahuje layout stránky
         a hlavičky pro rychlé zobrazení -->
</head>
<body>
     <div class="header">...</div>

     <link rel=stylesheet href="article.css">
     <div class="article">...</div>

     <link rel=stylesheet href="footer.css">
     <div class="footer">...</div>
 </body></html>

Firefox má v tomhle trochu problém, protože ve skutečnosti nepozastaví vykreslování stránky, pokud najde link v těle stránky a tak může dojít k vykreslení článku a patičky před tím, než bude k dispozici příslušný styl. Ošetřit se to dá tím, že do stránky přidáme (prázdný) skript, který vykreslování skutečně zastaví a počká na stažení stylů (protože neví, jestli skript nebude pracovat s definicemi stylů):

...
    <link rel=stylesheet href="article.css">
    <script> </script>
    <div class="article">...</div>
 
    <link rel=stylesheet href="footer.css">
    <script> </script>
    <div class="footer">...</div>
 
...

Skript může být prázdný, ale ne úplně prázdný, protože by ho Firefox ignoroval (protože je mu jasné, že nic dělat nemůže). Skript s mezerou je tedy dobrý kompromis, kdy prohlížeč neví, co přesně bude dělat, ale ve skutečnosti nic nedělá a tedy nezdržuje víc, než musí.

A zde také právě narážíme na rozdíl mezi IE a Webkitem (Chrome, Opera, Safari). Internet Explorer totiž v okamžiku, kdy čeká na stažení CSS souboru, může vykreslovat tu část stránky, kterou již má staženou. Tedy v okamžiku, kdy stahuje footer.css, může renderovat článek, protože CSS i HTML kód pro article už má k dispozici. Stejně se chová i Firefox v případě, že použijete trik s prázdných skriptem.

Naproti tomu Webkit při stahování stylů pozastaví nejen zpracování následného HTML kódu, ale také zastaví renderování předešlého kódu, které má ale již plně k dispozici. To má celkem logické vysvětlení, protože právě stahovaný soubor může (čistě teoreticky) obsahovat definice, které ovlivní již načtený kód. A toto se právě vývojáři Googlu snaží změnit, aby i Webkit mohl renderovat již zpracovaný HTML kód v okamžiku, kdy stahuje soubor stylů. Tato změna je momentálně (únor 2016) ve vývojové (Canary) verzi a mohli bychom se jí dočkat přibližně v Chrome 50+.

Update: Progresivní načítání CSS bylo přidáno do Chromium v lednu 2017 (Chrome 56+) a v březnu 2017 do Webkit (Safari 10.1+).

Využívání komponent

Tento systém načítání komponent je i příhodný pro frameworky, které generují HTML kód z komponent (tříd nebo šablon) na serveru. Každá komponenta tak může mít vlastní CSS soubor, který si sama vloží do stránky:

<?php
class HtmlComponent {
    protected static $css;
    protected static cssLoaded;
 
    protected function loadCss() {
        if (!static::$cssLoaded) {
            return '<link rel=stylesheet'
                   .' href="'.static::$css 
                   .'"><script> </script>';
            static::$cssLoaded = true;
        }
        return ''; //no CSS, already loaded
     }
}
 
class Article extends HtmlComponent {
    protected static $css = 'article';
    protected static cssLoaded = false;
    protected $data;
    public function __constructor($data) {
        $this->data = $data;
    }
    public function __toString() {
        $s = $this->loadCss();
        $s .= '<div class="article">';
        //... zpracuj $this->data
        $s .= '</div>';
        return $s;
    }
}
 
//výpis stránky
foreach ($articles as $article) {
    echo new Article($article);
}

Podobně můžete od HtmlComponent odvodit i ostatní části stránky jako je menu, galerie, patička, atd. a každá komponenta si sama stáhne příslušné styly, jen pokud je bude potřebovat.

Poznámka pro PHP: Všimněte si, že funkce loadCss() používá static::$css místo obvyklého self::$css, což je tzv. Late Static Binding dostupné od PHP 5.3.0, které zajistí, že každá komponenta bude používat vlastní statickou proměnnou místo jedné společné HtmlComponent::$ccs. Ostatní jazyky tohle budou muset řešit po svém aby zajistili, že každá komponenta bude používat vlastní proměnné css a cssLoaded.

Omezení stylů na komponentu

Aby styly ve staženém souboru platili skutečně jen na danou komponentu, musíte je samozřejmě omezit pomocí selektoru v definici pravidel. Nejjednodušším způsobem je přidat typ komponenty do cesty selektoru:

.article { ... }
.article p { ... }
.article a { ... }
.article img { ... }

Tento způsob ale není příliš efektivní, protože prohlížeč musí u každého pravidla ověřit cestu a navíc bude pravidla zkoušet aplikovat i na stejné prvky v jiných komponentách. Pokud se ale týká jen pár pravidel, jako v předchozím příkladu, dá se to snést. Pokud navíc víte, že daná komponenta bude mít jen pár úrovní zanoření prvků, můžete použít označení pro přímého potomka a tím omezit pravidla jen do dané určité stránky:

.article { ... }
.article > p { ... }
.article > a,
.article > p > a { ... }
.article > p > img,
.article > p > a > img { ... }

Samozřejmě nejefektivnější a nejrychlejší by bylo, aby každý prvek uvnitř komponenty měl vlastní třídu, která by označovala jeho typ a příslušnost ke komponentě:

.article { ... }
.article-paragraph { ... }
.article-link { ... }
.article-image { ... }

V tomto případě jsou všechna pravidla omezena na danou komponentu a zároveň nezpůsobují prohlížeči problém s hledáním pravidel pro jednotlivé elementy. Pak by ale bylo příhodné – pokud generujete HTML na serveru z komponent nebo tříd – vytvořit sub-komponenty pro jednotlivé prvky a vytvářet je pomocí nich:

class Article extends HtmlComponent {
    protected static $css = 'article';
    //...
    public function __toString() {
        $s = $this->loadCss();
        $s .= '<div class="article">';
        $p = new HtmlParagraph(self::$css);
        $s .= $p->open();
        $s .= $this->data['text'];
        $s .= $p->close();
        $a = new HtmlLink(self::$css);
        $a->setLink($this->data['link']);
        $s .= ''.$a; //zavolá toString()
        $s .= '</div>';
        return $s;
    }
}

Každá sub-komponenta tak může sama zajistit, aby její definice obsahovala správně strukturované jméno třídy pro označení jejího typu a příslušnosti ke komponentě, jejíž jméno jí dáte v konstruktoru. Pro generování těchto CSS tříd můžete použít princip BEM (Blok – Element – Modifikátor) nebo SuitCSS.

Podpora

Jak už bylo řečeno, Internet Explorer (IE9+) a Firefox již tento systém načítání CSS podporují. Chrome a Opera ho bude podporovat v řádu měsíců (odhadem 2. čtvrtina roku 2016) a Safari (jak je zvykem) se přidá někdy během roku (při vydání další verze iOS).

Důležité je, že starší verze Webkitu sice tento systém nevyužijí naplno, protože budou styly stahovat o něco pomaleji (kvůli rozdělení na řadu requestů), ale nebudou muset stahovat definice pro komponenty, které na stránce nejsou, a navíc budou profitovat z možnosti cachovat menší kusy a tudíž nebudou muset znovu stahovat styly komponent, které se od poslední návštěvy nezměnily.

A vzhledem k rychlosti rozšíření nových verzí webkitu do světa můžeme očekávat plnou podporu u více než 90% uživatelů počátkem roku 2017.

Chybná podpora v IE

I když celý tento princip vznikl na základě chování Internet Exploreru, právě tento prohlížeč má problém s jejich plnou podporou – a to v případě, že takto vytvořené komponentní CSS stáhnete přes AJAX. Při normálním (prvním) stažení stránky vše funguje správně. Když pak ale část stránky aktualizujete přes AJAX a odpověď obsahuje LINKy na komponentní CSS, Internet Explorer již tyto styly neaplikuje na nový kód – navíc vymaže starý kód (pokud jste přepsali obsah komponenty), takže AJAXem stažená komponenta ztratí styl (formátování).

Abyste to opravili, musíte projít AJAX odpověď, najít v ní všechny LINK prvky a zkopírovat je do hlavičky (<HEAD>). Při tom si dejte pozor na další chybu IE, protože musíte vytvořit nový LINK (nestačí jen přesunout ten z odpovědi) a nejprve ho musíte přidat do hlavičky a pak teprve mu nastavit atribut HREF.

Příklad používající jQuery a Nette Ajax:

$(function() {
    var ieStyleClass = 'ie-style-added-by-';
    $.nette.ext('ie-style', {
        complete: function(payload) {
            if (window.navigator.userAgent.indexOf('MSIE ') >= 0
              || !!navigator.userAgent.match(/Trident.*rv\:11\./))
            {
                if (payload && payload.snippets) {
                    $.each(payload.snippets, 
                      function(snippetName, code) {
                        var snippet = $.parseHTML(code);
                        $(document
                          .getElementsByClassName(
                          ieStyleClass + snippetName)
                        ).remove();
                        $(snippet)
                          .find('link[rel=stylesheet]')
                          .each(function(name, el) {
                            var $link = $(document
                              .createElement('LINK'));
                            $link.appendTo('head');
                            $link
                                .attr('type', 'text/css')
                                .attr('rel', 'stylesheet')
                                .attr('href', el.href)
                                .addClass(ieStyleClass 
                                  + snippetName)
                            ;
                        });
                    });
                }
            }
        }
    });
});

Pro jQuery nebo vanilla JS si kód upravte za domácí úkol.

Internet Explorer má ještě jeden nešvar. Většina prohlížečů použije LINK pro načtení stylů, pokud má atribut rel="stylesheet" nebo type="text/css", a je jedno, který z nich použijete. V IE ale musíte použít oba, protože jinak LINK nemusí fungovat správně pro komponentní CSS nebo pro zde uvedenou opravu AJAXu.

Inlinování důležitých CSS

V úvodu článku je uvedeno, že CSS důležité pro zobrazí stránky, by mělo být nalinkováno v hlavičce:

<html><head>
    <link rel=stylesheet href="main.css">
    <!-- main.css obsahuje layout stránky
         a hlavičky pro rychlé zobrazení -->
</head>

Tím prohlížeči řeknete, že než bude zpracovávat HTML z BODY, musí stáhnout externí CSS. To může být problém, pokud je připojení pomalé (3G) a vytvoření spojení trvá dlouho, protože než se naváže spojení, prohlížeč nemůže nic renderovat.

Řešením, pokud je vaše above-fold CSS relativně malé, vložit ho přímo do HTML hlavičky (inline). Díky tomu nemusí prohlížeč nic stahovat a jen CSS zkompiluje a může pokračovat v renderování HTML.

<html><head>
    <style><?php readfile(DIR_WWW.'/main.css') ?></style>
    <!-- main.css obsahuje layout stránky
         a hlavičky pro rychlé zobrazení -->
    <!-- funkce readfile() načte soubor a pošle ho na výstup -->
</head>

Při tomto řešení je ale potřeba dát velký pozor na to, co do hlavního CSS dáváte, aby v něm nebyly zbytečnosti jako jsou CSS utility (např. .highlight) a jiné nepotřebné styly (např. .btn:hover), které můžete stáhnout až později na konci stránky nebo asynchronně.

Pokud do hlavního CSS začnete vkládat nepotřebné styly jako je reset, utility, animace a jiné pomocné třídy, budou se tyto definice stahovat s každým navštívením stránky a bude to zpomalovat procházení, protože se nemohou uložit do cache.

Pokud používáte PHP či jiný templatovací systém schopný použít Session nebo Cookie, můžete udělat to, že při prvním načtení vložíte CSS přímo do HTML a následně do Session nebo Cookie uložíte čas či jinou informaci. Při generování druhé navštívené stránky pak kromě inline stylu přidáte ještě preload odkazy, které stáhnou soubory na pozadí a uloží je do cache. Při generování dalších stránek pak již můžete místo inline stylů vkládat přímo odkazy na externí soubory, které již máte uloženy v cache, takže již budou rychlé a úsporné. Alternativně místo druhé stránky s inline a preload můžete použít Javascript na konci stránky, který preload odkazy přidá asynchronně.

Z pohledu uživatele se stane to, že úplně první návštěva bude hodně rychlá, protože se vše potřebné (pro above-fold) stáhne v jediném dotazu (na HTML). Druhé zobrazení pak bude o něco pomalejší, protože kromě inline CSS bude obsahovat preload odkazy (ale zároveň už se stáhla část below-fold CSS) a třetí a další zobrazení bude ještě rychlejší než první, protože už se CSS stahovat nebude (resp. jen nové komponenty).

Asynchronní stahování CSS

Pokud chcete stahovat část (komponentních) CSS (které jsou „below fold“ asynchronně (tedy až po stažení celé stránky), můžete použít trik s tiskem:

<link rel=stylesheet href="css/layout.css" 
      media=print onload="this.media='all'">

Odkaz na CSS ho nejprve označí jako styl pro tisk (media=print), což většina prohlížečů pochopí tak, že ho nepotřebuje pro vykreslení stránky (media=screen) a tudíž stažení odloží až na čas, kdy stahují preload zdroje (tedy po onload stránky). Toto řešení samo o sobě slouží jako fallback, protože prohlížeč, který media=print nepochopí jako asynchronní stažení, jednodu3e stáhne soubor synchronně a nic ne nemění.

Po stažení příslušného CSS pak pomocí Javascriptu přepneme zobrazení, aby se styly aplikovali i na zobrazenou stránku (media=all). Nevýhoda tohoto řešení je v tom, že bez Javascriptu se soubor stáhne, ale nepoužije, takže stránka bude vypadat jinak (záleží na tom, jak moc je váš layout závislí na CSS).

Nutnost použít JS by se dala obejít tím, že za první odkaz přidáte druhý do noscript, čímž prohlížeč s vypnutým Javascriptem donutíte stáhnout soubor synchronně a použít okamžitě, ale zároveň zvětšujete velikost HTML (pro všechny návštěvníky), které se (na rozdíl od CSS) stahuje při každé návštěvě stránky. Záleží pak na poměru velikosti (všech asynchronních) CSS a frekvenci návštěvy, zda kvůli urychlení prvního zobrazení stránky nesnížíte celkovou rychlost procházení stránek.

<link rel=stylesheet href="css/layout.css" 
      media=print onload="this.media='all'">
<noscript>
    <link rel=stylesheet href="css/layout.css" media=all>
</noscript>

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *

Tato stránka používá Akismet k omezení spamu. Podívejte se, jak vaše data z komentářů zpracováváme..