Optimalizace selektorů

Možná vás již napadlo, jak rychle je prohlížeč schopen najít CSS pravidlo pro konkrétní element v HTML a co dělat nebo nedělat pro to, abyste mu co nejvíce pomohly.

tl;dr: Pokud nechcete číst celý článek, podívejte se na shrnutí na jeho konci.

V jednoduchosti je síla

Nejrychlejší pravidlo je takové, které přesně specifikuje prvek. Nejde přitom ale o to, kolik takových prvků v HTML je, ale jak snadno a rychle může prohlížeč určit, že daný prvek spadá do daného pravidla.

Nejrychlejší tedy logicky je, pokud pravidlo obsahuje ID:

#menu { float: left; width: 30%; }

Toto pravidlo jednoduše říká, že prvek menu bude nalevo – když prohlížeč najde prvek s ID „menu„, pravidlo použije a nemusí nad ničím přemýšlet.

Překvapivě ale stejně rychlá jsou i pravidla, která definují styl pomocí třídy nebo tagu:

a { decoration: none; color: black; }
.highlight { background: yellow }

Zde také nemusí prohlížeč dlouho přemýšlet – když najde odkaz, zruší mu podtržení a nastaví barvu, a když najde prvek se třídou highlight, nastaví mu pozadí.

Sice o něco pomalejší, ale stále dostatečně rychlá jsou pravidla, spoléhající se na vlastnost prvku:

[disabled] { color: gray }
[type=password] {
    background:white; color:white;
}
/* secured links */
[href^=https://] { color:red }

I když v tomto případě musí prohlížeč sáhnout k tomu, že ověří vlastnost prvku, zda je disabled nebo má typ password, což je o trochu pomalejší než kontrola třídy, ID nebo tagu, pořád je to hodně rychlé, protože prohlížeč stále pracuje s jedním prvkem a nemusí tedy nic složitě dohledávat (oproti postupům popsaným v dalších kapitolách).

V posledním případě už bude zpracování přeci jen pomalejší, protože nejenže musí prohlížeč přečíst vlastnost HREF, ale ještě musí pomocí regulárního výrazu ověřit, zda hodnota vlastnosti začíná určeným řetězcem. Vždy si tedy dobře rozmyslete, zda opravdu potřebujete takové pravidla – pokud pracujete na internetovém bankovnictví, kde je potřeba uživatele upozornit, zda pracuje s bezpečnými odkazy, určitě takové pravidlo dává smysl; pokud jen chcete ozvláštnit váš blog o siamských kočkách, raději dejte na stránku o jeden obrázek víc, než abyste zdržovali vykreslování CSS pravidel.

Uvědomte si, že pravidlo kontrolující vlastnost je nejrychlejší, pokud jen ověřuje existenci (např. [disabled]). O něco pomalejší je kontrola hodnoty (např. [type=text]). Ještě pomalejší je, pokud použijete nějaké speciální porovnání, např. hledání slov [role~=link], full-text [src*=iphone] nebo regulární výraz [href$=.php]. Nedává tak smysl často doporučovaný způsob kontroly typu inputu přes [type|=text], protože zbytečně používá podmíněné hledání (začátek slov) na vlastnost, jejíž hodnoty jsou známy a nijak se neopakují.

Více jmen škodí

Jak by jistě potvrdil Jan Josef Václav Antonín František Karel Radecký z Radče, mít moc jmen škodí (pokud znáte Cimrmanovo České nebe, tak více proč).

Stejně tak i v CSS, pokud uvedete více rovnocenných identifikátorů, zbytečně zpomalíte proces rozhodování:

#backToHome { color: red; }  /* rychlé */
a#backToHome { color: red; } /* pomalé */

U elementu, který se jmenuje backToHome se dá předpokládat, že to bude odkaz na domovskou stránku; navíc tím, že to je ID, tak musí být unikátní. Tak proč ještě uvádět, že jde o odkaz (Anchor)? V takovém případě totiž musí prohlížeč, když najde element #backToHome, ověřit, zda jde skutečně o odkaz, což mu samozřejmě nějaký čas zabere.

Stejně tak, pokud chcete nastavit styl pomocí třídy, ničemu nepomůžete, když pravidla více specifikujete:

p.highlight, a.highlight {
             background: yellow } /* pomalé */
.highlight { background: yellow } /* rychlé */

Zde dojde k tomu, že po nalezení odstavce nebo odkazu, musí prohlížeč ověřit, jestli má danou třídu – a zbytečně porovnává dvě vlastnosti. U druhého pravidla mu totiž stačí jen ověřit, jakou má třídu, a podle toho pravidlo použít nebo ne.

Pokud tedy chcete použít konkrétní třídu pro konkrétní styl, prostě nadefinujte jednoduché pravidlo – a pak se třeba s kolegy dohodněte, že danou třídu nebudete používat pro nic jiného.

Pokud uvažujete o tom, že jednu třídu použijete v různých situacích pro různé styly…:

/* pomalé! */
p.highlight { background: yellow }
div.highlight { border: 2px solid yellow }

… tak si ho hodně rychle rozmyslete. Proč nutit prohlížeč při každém překreslení rozhodovat, zda zvýrazňuje odstavec nebo blok, když to může rozhodnout programátor již při přípravě stránky:

/* rychlé */
.highlight-text { background: yellow }
.highlight-block { border: 2px solid yellow }

Samozřejmě je potřeba se rozhodnout, zda daná kombinace stylů dává smysl – pokud chcete prvku přidat třídu jen proto, aby v kódu bylo vidět, jak bude vypadat, tak je to nesmysl. Pokud naopak používáte třídu k tomu, abyste dynamicky měnili jeho vzhled, je to zcela v pořádku (i když…):

#menu.border { border: 1px solid gray } /*
   nesmysl - pokud má mít menu border vždy,
   nastavte to přímo pro tag: */
#menu { border: 1px solid gray } /* OK */

/* v pořádku
   změní styl až po změně třídy */
#iAgree { background: red }
#iAgree.checked { background: green }

/* i když tohle by bylo ještě rychlejší */
.agree-unchecked { background: red }
.agree-checked { background: green }

Výjimku v tomto případě tvoří ověření vlastnosti (jak bylo uvedeno dříve):

/* rychlejší (než výše uvedené) */
button[disabled] { color: gray; }
a[href^=https://] { color: red }

Zde totiž (moderní) prohlížeč správně rozpozná, že v pravidle je podstatný typ (tedy button nebo A) a určení vlastnosti je jen upřesňující. Pak tedy nejprve ověří, že nalezený prvek je tlačítko nebo odkaz a pak teprve kontroluje vlastnost – nemusí tedy ověřovat vlastnosti disabled a href u odstavců nebo obrázků a rozhodování se urychlí.

Všechny cesty vedou do Rootu

Pokud si myslíte, že specifikováním cesty k prvku prohlížeči nějak usnadníte jeho nalezení…:

html body ul#menu li.selected a:visited {
    color: blue
}

… tak evidentně nevíte, jak prohlížeč pravidla aplikuje.

Tohle by totiž platilo v případě, že by prohlížeč vzal zmíněné pravidlo a pak procházel HTML a hledal, na které prvky se hodí. A skutečně to platí v případě hledání prvku pomocí jQuery selektoru.

Aplikování CSS pravidel ale funguje opačně: prohlížeč ví, že zrovna vykresluje odkaz, který byl již navštíven a tak ví, že má hledat pravidla, která mají na konci „a:visited„. A pokud najde pravidlo, které obsahuje cestu, musí pak projít všechny rodiče daného prvku a u každého ověřit, zda do daného pravidla zapadá.

Tudíž čím více prvků do cesty uvedete, tím pomaleji bude zpracování probíhat. Pokud tedy potřebujete upřesnit cestu, je nejlepší použít operátor přímého potomka:

.menu a:visited { color: blue } /* pomalé
    pokud není odkaz uvnitř menu, musí
    prohlížeč dojít až do rootu, aby se
    o tom přesvědčil */

.menu > li > a:visited { color: blue } /*
    rychlejší - pokud odkaz není v seznamu,
    prohlížeč už dál nehledá;
    stejně jako pokud to není uvnitř menu */

Stejně tak vzdálenost mezi prvky rozhoduje o rychlosti aplikace pravidla v případě, že pasuje:

.menu a:visited { color: blue } /* rychlé
    pokud je odkaz přímo v menu, najde ho
    prohlížeč celkem rychle */

html.showMenu a:visited { color: blue } /*
    pomalé - pro každý odkaz musí prohlížeč
    projít všechny jeho rodiče až do rootu
    a ověřit, jestli má danou třídu */

Pokud už potřebujete aplikovat nějaké styly podle třídy, která je v hierarchii někde nahoře (typicky v HTML nebo BODY), použijte nějakou třídu na daný prvek, pomocí které určíte, zda má vůbec cenu cestu do rootu prohledávat:

html.showMenu a:visited
 { color: blue } /*
  pomalé - pro každý odkaz musí prohlížeč
  projít všechny jeho rodiče až do rootu
  a ověřit, jestli má danou třídu
*/
html.showMenu #menu > a:visited
 { color: blue } /*
  rychlejší - do rootu se prohlížeč podívá
  jen pokud ví, že zpracovává odkaz uvnitř
  menu; u ostatních bude pravidlo ignorovat
*/
html.showMenu a.menuItem:visited
 { color: blue } /*
  nejrychlejší - do rootu je prohlížeč
  podívá jen pokud ví, že zpracovává
  přímo k tomu účelu určený prvek
  (položku menu) */

I u cesty platí stejná věc jako u pravidla bez cesty – čím méně selektory prvek určíte, tím rychleji bude probíhat jeho ověření:

ul#menu.opened li#home.selected
     a.highlight:visited { color: black }
  /* pomalé, každý prvek je určen několika
  vlastnostmi, což prohlížeči nepomáhá */

#menu #home a:visited { color: black }
  /* rychlé, jen ověří typ nebo ID a jde dá */

U zpracování cesty platí, že prohlížeč nejprve ověřuje ID, typ nebo třídu. V druhé řadě se dívá na rozšiřující definice prvku, tedy vlastnosti nebo speciální selektory (např. „:visited„) a teprve na závěr se zaměřuje na cestu k prvku, přičemž postupuje od samotného prvku směrem nahoru k rootu. A pokud kdekoliv v tomto postupu zjistí, že něco neodpovídá, pravidlo zahodí a již ho dále neověřuje.

Rychlost vs. Priorita

Vše výše popsané má samozřejmě smysl, pokud vás zajímá čistě jen rychlost. U CSS je ale potřeba brát v úvahu i prioritu jednotlivých selektorů a pořadí jejich zpracování.

Například v situaci, kdy chceme všem položkám v menu nastavit barvu pozadí (a nemůžeme jim, z jakéhokoliv důvodu, dát konkrétní třídu) a jedné konkrétní jinou, by z hlediska rychlosti bylo nejlepší:

.menu > a { color: black }
.menu_current_item { color: gray }

Dle očekávání později definovaný styl by měl přepsat ten dřívější. Jenomže selektor se dvěma prvky má vyšší prioritu než selektor jen s jedním a proto bude výslední barva černá.

Aby tedy styl fungoval správně, budete muset sáhnout k o něco pomalejšímu ale prioritně správnému:

.menu > a { color: black }
.menu > .menu_current_item { color: gray }

Pro urychlení můžete použít jeden trik. Prohlížeč při určování priority pravidla sčítá ID, třídy, tagy, atd., ale již se nedívá na jich význam. Pokud tedy do pravidla uvedete třídu nebo tag, který nezmění jeho význam, pořád se tím změní priorita a tak můžete urychlit jeho vyhodnocení. Navíc nepotřebujete přidávat do HTML další třídy jen proto, že jsou potřeba pro CSS, protože můžete zopakovat ty, které tam již jsou.

/* Pomalé, protože používá cestu */
.menu > .item { color: gray; }
div > span > img { float: right; }

/* rychlejší */
.item.item { color:gray; }
/* sice pomalejší než jedna třída, ale
  rychlejší než hledání rodiče */
img:not(span):not(div) { float:right}
/* obrázek nemůže být DIVem ani SPANem,
  ale rozhoduje to o prioritě */

Samozřejmě, pokud použijete podobný trik se zdvojením třídy, uvedením nesmyslných tagů nebo jen uvedete více selektorů pro zvýšení priority, je vhodné uvést k tomu komentář, proč to děláte a jaké pravidlo se snažíte přepsat. Za prvé, až se později vy nebo někdo jiný bude dívat do CSS, nebude muset přemýšlet, co tím autor myslel (a jestli u toho vůbec myslel) a za druhé pokud uvedete odkaz na pravidlo, usnadní to později refaktoring, pokud původní pravidlo vyřadíte, budete vědět, že můžete vyřadit i zdvojené třídy nebo tagy.

Podobný problém nastane v případě, že používáte sice rychlé sektory pomocí ID, ale pro prioritu nevhodné:

/* základní CSS */
#menu_contact {
    background: url(contact.png) red;
}
/* Theme - všechny menu položky budou modré */
.menu > .menu_item > a {
    background-color: blue;
}

V tomto případě bude mít odkaz na kontakty červenou barvu na pozadí, protože první selektor pomocí ID má vyšší prioritu a tak překryje později definovaný styl, i když má delší a (zdánlivě) přesnější cestu.

Existuje i názor, kterého se drží dokonce i některé validátory, že v CSS by se vůbec neměli používat selektory s ID prvku. Důvod je právě ten, že ID má nejvyšší prioritu a tak v případě, že definujete něco pro konkrétní prvek, nemůžete pak snadno změnit vše pomocí méně konkrétního stylu. Výše uvedený příklad tedy nebude fungovat podle očekávání, kdy chcete pomocí šablony změnit barvu všech položek v menu.

Lepší tedy je místo ID používat unikátní třídy:

/* základní CSS */
.menu_item_contact {
    background: url(contact.png) red;
}
/* Theme - všechny menu položky budou modré */
.menu > .menu_item > a {
    background-color: blue;
}


Ještě existuje jeden nešvar – potřebuji přepsat pravidlo, které je uvedené v později načítaném souboru a tak uvedu více selektorů, aby pravidlo získalo vyšší prioritu:

a.menu_item.contact:visited
 { color: red } /* pomalé,
 protože musím přepsat všechny
 pozdější pravidla protože jsem líný,
 a nechci to dávat jinam */
/* ... */
a { color: blue }
a:visited { color: blue }
.menu_item { color: black }

Přitom stačí pravidla jen správně seřadit, nebo přesunout do jiného (později načítaného) souboru a vše může fungovat rychle a spolehlivě.

Priority podrobněji

Abychom správně pochopili, jak prohlížeč porovnává pravidla, je potřeba vědět, jak počítá tzv. váhu pravidla.

Většinou, když se to vysvětluje začátečníkům, řekne se něco jako: typ prvku přidá váhu 1, třída přidá váhu 10 a ID přidá váhu 100. Takže selektor se dvěma tagy a jednou třídou má váhu 12.

Tohle vysvětlení je správné, ale zavádějící, protože by mohl vzniknout dojem, že selektor se 13 typy prvků bude mít váhu 13 a tudíž bude mít vyšší váhu než selektor se 2 typy a jednou třídou (váha 12).

Váha se totiž nepočítá jako jedno číslo a tudíž se desítky nepřenáší. Správně by se totiž měla váha uvádět jako trojice čísel, např. místo 12 je lepší uvést 0-1-2, kde první číslo je počet ID, druhé číslo je počet tříd a třetí číslo je počet ostatních selektorů.

Když pak tedy správně zapíšeme obě výše uvedené váhy, vyjde nám 0-1-2 a 0-0-13. Jelikož se váha porovnává zleva doprava vždy po skupinách, je vidět, že první váha vítězí s jednou třídou oproti žádné a k porovnání třetího čísla (2 proti 13) již nedojde.

Do první skupiny (1-0-0) se započítávají všechny ID (tedy #ID) ze selektoru. Do druhé skupiny (0-1-0) se počítají třídy (.class) a pseudo-třídy, které mají vlastní význam (např. :first-child, :active, apod.). Do třetí třídy (0-0-1) se počítají ostatní selektory jako tagy (div, span, atd.), vlastnosti (např. [type=number]), apod. Ostatní selektory (např. > nebo +) žádnou váhu nepřidávají. Stejně tak nepřidává žádnou váhu obecný selektor * (protože se nemusí uvádět a *.link má stejný význam jako .link).

Speciální pseudo-třída :not() sama nepřidává žádnou váhu (tedy nepřidává 0-1-0, i když je to pseudo-třída), ale přidá váhu selektoru, který je v závorce. Tedy např. selektor a.link:not(:visited) má váhu 0-2-1 (tag a + třída .link + pseudo-třída :visited) zatímco selektor div > :not(span) má váhu 0-0-2 (div + span).

Klíčové slovo !important pak přidá do váhy nultou skupinu, takže např. #menu { color:black !important; font-size: 10px } bude mít váhu 1-1-0-0 pro vlastnost color, takže pro její přepsání bude potřeba pozdější pravidlo s váhou 1-1-0-0 nebo dřívější pravidlo s váhou 1-2-0-0, 1-1-1-0, 1-1-0-1 apod.

Pozor ale na to, že !important nemění váhu celého pravidla, takže uvedené pravidlo pro #menu má stále váhu 1-0-0 a dříve uvedené pravidlo např. #sidebar > #menu { color: white; font-size: 12px } sice nevyhraje u color, protože jeho priorita 0-2-0-0 je míň než 1-1-0-0 u #menu), ale vyhraje u font-size, protože jeho 2-0-0 přebije 1-0-0 u #menu.

Shrnutí

U CSS tedy platí, že čím kratší pravidlo napíšete, tím rychleji se bude prohlížeči zpracovávat a čím konkrétnější budete, tím rychleji prohlížeč pozná, že už nemá smysl dál hledat.

  1. Definujte prvek jen jedním identifikátorem (ID, typ nebo třída), pokud je to dostatečně specifické.
  2. Při kontrole vlastnosti ještě specifikujte typ prvku, aby se nekontrolovala u prvku, který ji mít ani nemůže.
  3. Nekombinujte více identifikátorů určující ten samý prvek (např. ID + typ nebo typ + třída).
  4. Používejte třídy pouze pro konkrétní případy; nedefinujte obecnou třídu, která nastavuje pokaždé jiný styl.
  5. Nespecifikujte cestu k prvku tam, kde není potřeba.
  6. Používejte operátor přímého potomka tam, kde to dává smysl.
  7. Při použití cesty se snažte co nejpřesněji specifikovat výchozí prvek (poslední seskupení v cestě).

Pamatujte ale také na prioritu pravidel:

  1. Pravidlo definované pomocí ID znemožní pozdější změnu pomocí třídy nebo typu – místo ID používejte unikátní třídy.
  2. Pravidlo bez cesty nebo s kratší cestou má menší prioritu než dříve uvedené pravidlo s (delší) cestou – používejte cestu tam, kde je to potřeba pro přepsání jiných pravidel.
  3. Selektor pro přímého potomka (parent > child) má stejnou prioritu jako obecný potomek (parent child) – používejte ho kdykoliv to urychlí hledání (protože prioritu nezmění).
  4. Pravidla se stejnou prioritou se snažte správně seřadit místo toho, abyste používali přesnější selektory nebo !important.

Napsat komentář

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