Selektory#
Profile i wydajność#
Z selektorami związanych jest kilka kwestii wydajnościowych. Z praktycznego punktu widzenia implementacje CSS (w tym selektory) są bardzo szybkie, ale warto wiedzieć, z jakimi problemami w przyszłości możemy się zetknąć. W tym miejscu omówię najważniejsze kwestie związane z szybkością przetwarzania selektorów.
Profile selektorów#
Selektory są wykorzystywane w wielu różnych kontekstach o wyjątkowo różnych charakterystykach wydajnościowych. Niektóre selektory o ogromnych możliwościach są niestety zbyt wolne, aby mogły być rzeczywiście zawarte w wielu kontekstach czułych na wydajność. Aby to pogodzić specyfikacja selektorów definiuje dwa profile (profile):
- szybki (fast) #
Szybki profil jest odpowiedni do stosowania w każdym kontekście, włącznie z dynamicznym dopasowaniem selektorów CSS przez przeglądarkę. Obejmuje każdy selektor definiowany w tej specyfikacji, z wyjątkiem:
- Kombinatorów wewnątrz:
:matches()
,:not()
,:nth-match()
i:nth-last-match()
.Wciąż trwa dyskusja, czy ograniczenie to nie zostanie zniesione.
- Kombinatora referencji
- Wskaźnika tematu
- Kombinatorów wewnątrz:
- kompletny (complete) #
Kompletny profil jest odpowiedni dla kontekstów, które nie są wrażliwe na wydajność. Dla przykładu, implementacje specyfikacji "Selectors API" powinny używać kompletnego profilu. Obejmuje on wszystkie selektory zdefiniowane w tej specyfikacji.
Implementacje CSS zgodne z modułem "Selectors Level 4" muszą korzystać z szybkiego profilu dla selekcji CSS.
Wydajność selektorów#
Stosowanie reguł CSS bezpośrednio w elementach <style>
jest na tyle wydajne, że z praktycznego punktu widzenia mało kto zawraca sobie tym głowę. Dotyczy to także selektorów, których czas przetwarzania jest zazwyczaj zadowalający. Tak czy inaczej warto zdawać sobie sprawę z tego, że i w tym aspekcie można przeprowadzić pewne usprawnienia naszego kodu.
Selektory przeprowadzają selekcję spośród elementów, dlatego im mniej kroków do wykonania, tym cały proces przebiegnie szybciej. Liczba dopasowań, jakie musi przeprowadzić przeglądarka, zależy od sposobu, w jaki napisane są selektory. Droga do tworzenia wydajnych selektorów rozpoczyna się od zrozumienia sposobu, w jaki przebiega takie dopasowanie.
Kierunek przetwarzania selektorów#
Rozważmy następujący selektor:
#box p {color: red;}
Zgodnie z teoretycznym opisem określania selektora przeglądarka powinna wybrać spośród wszystkich elementów dokumentu ten, który ma identyfikator "box"
, a następnie wybrać wszystkie zawarte w nim akapity. Mamy więc do czynienia z przetwarzaniem selektora od lewej do prawej. W naszym przykładowym selektorze jest to dość wydajne, gdyż najpierw wybierany jest tylko jeden element (dwa elementy o tym samym identyfikatorze w dokumencie są zabronione), po czym następują kolejne kroki selekcji.
W rzeczywistych implementacjach CSS przeglądarki dopasowują selektory od prawej do lewej #, co jest łatwiejsze w implementacji i w pewnych sytuacjach wydajniejsze (przy odpowiednim konstruowaniu selektorów). Informację tę po raz pierwszy ujawnił David Hyatt już w 2000 roku, architekt przeglądarki Safari i silnika WebKit, w jednym z najczęściej przytaczanych artykułów na temat wydajności selektorów CSS - "Writing efficient CSS".
Uwzględniając informacje tam zawarte, rzeczywiste działanie naszego selektora jest następujące: spośród wszystkich elementów dokumentu wybierz te, które są akapitami. Następnie dopasuj jedynie te, których przodkiem jest element z identyfikatorem "box"
. Rezultat obydwu rozwiązań powinien być taki sam (i jest dopuszczalny przez Selektory), ale widać wyraźnie, że w tym wypadku drugie podejście wymaga większych nakładów pracy. Zamiast po prostu sprawdzić elementy p
znajdujące się wewnątrz pojedynczego elementu z identyfikatorem "box"
, jak byłoby w przypadku odczytywania od lewej do prawej, przeglądarka musi wydobyć wszystkie akapity z dokumentu, a następnie piąć się w górę przez całe drzewo węzłów aż do korzenia dokumentu, szukając przodka o odpowiednim identyfikatorze.
W ramach uzupełnienia proponuję przeanalizować następujące wpisy ze StackOverflow: "Why do browsers match CSS selectors from right to left?", "Is it easier/faster to evaluate CSS selectors LTR or RTL?", "CSS combinator precedence?" i "CSS selector engine clarification?".
Praktyczne uwagi#
Wiedząc, że selektory są dopasowywane od prawej do lewej, możemy tworzyć nieco wydajniejsze konstrukcje selektorów. Wszystkie selektory przynależą do następujących kategorii #, zgodnie z ich priorytetem przetwarzania:
- Identyfikator
- Klasa
- Typ
- Grupa uniwersalna
Najpierw ustalany jest tzw. selektor kluczowy #, czyli zwycięski selektor prosty w ostatnim selektorze złożonym, który wskazuje na startowy zestaw elementów. Zgodnie z powyższą listą, pierwszeństwo ma tutaj selektor identyfikatora, następnie klasy, nazwy tagu i pozostałe warianty. Wszystko zależy od obecności poszczególnych selektorów prostych. Oto kilka przykładów dla każdego wariantu:
/* Kategoria identyfikatora */
button#backButton { } /* Selektor kluczowy #backButton */
#urlBar[type="autocomplete"] { } /* Selektor kluczowy #urlBar */
treeitem > treerow > treecell#myCell:active { } /* Selektor kluczowy #myCell */
/* Kategoria klasy */
button.toolbarButton { } /* Selektor kluczowy .toolbarButtonl */
.fancyText { } /* Selektor kluczowy .fancyText */
menuitem > .menu-left[checked="true"] { } /* Selektor kluczowy .menu-left */
/* Kategoria typu */
td { } /* Selektor kluczowy td */
treeitem > treerow { } /* Selektor kluczowy treerow */
input[type="checkbox"] { } /* Selektor kluczowy input */
/* Kategoria Grupy uniwersalnej */
[hidden="true"] { } /* Selektor kluczowy [hidden="true"] */
* { } /* Selektor kluczowy */
tree > [collapsed="true"] /* Selektor kluczowy [collapsed="true"] */
Mechanizm selektorów rozpoczyna dopasowywanie od selektora kluczowego i stopniowo przesuwa się w lewo. Praca zostanie przerwana w chwili pełnego dopasowania lub w sytuacji, gdy któryś z selektorów cząstkowych nie zwróci pozytywnego wyniku.
Najważniejszą koncepcją do zrozumienia będzie etap filtrowania kategorii. Kategorie utworzono w celu odfiltrowania zbędnych selektorów, dzięki czemu mechanizm nie marnuje czasu na niepotrzebne dopasowania. Jest to niezbędne do zwiększenia wydajności przetwarzania selektorów. Im mniej selektorów należy przetworzyć i dopasować do elementów, tym szybsze zastosowanie reguł CSS.
Dla przykładu, jeśli w selektorze złożonym występuje selektor identyfikatora (obok innych selektorów prostych), to w pierwszej kolejności on zostanie przetworzony. Nie ma potrzeby przeprowadzania nadmiarowych dopasowań dla pozostałych selektorów w sytuacji, kiedy dopasowanie identyfikatora nie wystąpi. Jeśli nie ma selektora identyfikatora, ale jest selektor klasy, to w pierwszej kolejności tylko on zostanie sprawdzony. Najgorszą charakterystykę wydajnościową mają selektory zaliczane do grupy uniwersalnej, ponieważ każdy z nich podlega analizie.
Możemy zatem stwierdzić, że kierunek przetwarzania selektorów od prawej do lewej dotyczy selektorów złożonych, które rozdziela jakiś kombinator. Jednakże sposób przetwarzania zawartych w nich selektorów prostych może zależeć od danej implementacji:
div.name[data-foo="bar"]:nth-child(5):hover::after
div:hover[data-foo="bar"].name:nth-child(5)::after
Obydwa selektory powyższego przykładu są funkcjonalnie identyczne i różnią się jedynie kolejnością występowania selektorów prostych. Wymienione wcześniej kategorie filtracji pochodzą z Firefoksa i pozwalają oszacować pierwszeństwo analizy selektorów niezależnie od kolejności rzeczywistego ich występowania. Niestety są to informacje zbyt ogólnikowe, by możliwe było zinterpretowanie kolejności przetwarzania każdego rodzaju selektora.
Oto kilka praktycznych wskazówek konstruowania wydajnych selektorów polecanych przez Davida Hyatta:
- Unikaj grupy uniwersalnej #
Oprócz tradycyjnego selektora uniwersalnego David zalicza tu wszystko prócz selektora identyfikatora, selektora klasy i selektora typu. Grupa ta jest na tyle ogólna, że selektory w niej zawarte mają negatywny wpływ na całkowitą wydajność selektora. Najlepiej trzymać się zasady, zgodnie z którą selektorem kluczowym nigdy nie powinien być selektor przynależący do grupy uniwersalnej.
- Nie precyzuj selektora identyfikatora
Ponieważ w dokumencie może występować tylko jeden element o danym identyfikatorze, to nie ma potrzeby podawania dodatkowych selektorów, prócz selektora identyfikatora:
/* Pisz zatem */ #backButton { } #newMenuIcon { } /* Zamiast */ button#backButton { } .menu-left#newMenuIcon { }
- Nie precyzuj selektora klasy
Zamiast uściślania selektora klasy do określonego elementu (za pomocą selektora typu), rozszerz nazwę klasy, tak aby była przystosowana do określonego użycia. Klasy mogą być stosowane wielokrotnie w tym samy dokumencie, ale wciąż pozostają bardziej konkretyzujące niż nazwy elementów:
/* Pisz zatem */ .li-chapter { } /* Lub jeszcze lepiej */ .list-chapter { } /* Zamiast */ li .chapter { }
- Twórz możliwe jak najbardziej konkretyzujące selektory
Największym powodem spowolnienia jest stosowanie zbyt wiele selektorów, głównie selektorów typu. Lepiej utworzyć dedykowaną klasę i przypisać ją do odpowiednich elementów:
/* Pisz zatem */ .li-anchor { } /* Zamiast */ ol li a { }
- Unikaj selektora potomka
Selektor potomka zaliczany jest do najbardziej kosztownych selektorów w całym CSS. Jego negatywny wpływ jest szczególnie odczuwalny w połączeniu z innymi selektorami zaliczanymi do grupy uniwersalnej. Problem jest na tyle istotny, że stosowanie selektora potomka zostało zablokowane w regułach CSS odnoszących się do UI przeglądarki Firefox, bez konkretnego uzasadnienia. Podobnie można postąpić w naszych własnych stronach WWW. W zastępstwie można skorzystać z selektora dziecka, choć do ideału wiele brakuje (patrz kolejny punkt):
/* Pisz zatem */ ol > li > a { } /* Zamiast */ ol li a { }
- Unikaj selektora dziecka
Selektor dziecka ma mniejszy wpływ na wydajność niż selektor potomka, ale i tak odradza się jego używania. Tam gdzie to możliwe zaleca się zastąpienie selektora dziecka odpowiednią klasą:
/* Pisz zatem */ .li-anchor { } /* Zamiast */ ol > li > a { }
- Polegaj na dziedziczeniu
Dowiedz się, które właściwości są dziedziczone, i unikaj reguł ponownie określających takie dziedziczone style. Dla przykładu, właściwości rodzica typu
list-style-image
lubfont
będą dziedziczone przez jego zawartość i ponowna ich deklaracja w elementach potomnych jest zbędna- Stosuj zawężony styl wewnętrzny
Mechanizm zawężania stylów pozwala ograniczyć liczbę dopasowywanych elementów, przez co proces przetwarza selektorów ulega skrócenia. Prawdę mówiąc jest to nowość właściwa dla kolejnych wersji języków CSS oraz HTML, i minie jeszcze wiele czasu, nim całość upowszechni się na dobre.
Widać wyraźnie, że stosowanie się do wskazówek Davida Hyatta polega głównie na wykorzystaniu selektora identyfikatora i selektora klasy. Takie rozwiązanie generuje efekty uboczne: zamiana selektora potomka czy dziecka na dedykowane identyfikatory i klasy zwiększa wynikowy rozmiar strony oraz zmniejsza elastyczność arkuszy CSS. Najważniejsze selektory, które należałoby poprawić, to te zawierające selektor kluczowy, który wybiera największą liczbę elementów początkowych, będących przedmiotem filtracji dla kolejnych selektorów znajdujących się po jego lewej stronie.
Wystarczy porównać ze sobą dwa następujące selektory:
div div div p a.class006
a.class006 *
Na pierwszy rzut oka wydaje się, że pierwszy selektor jest bardziej kosztowny (biorąc pod uwagę dużą liczbę zawartych w nim selektorów prostych), ale praktyka pokaże coś innego. Selektorem kluczowym dla pierwszego wariantu jest .class006
, co prawdopodobnie oznacza tylko jeden element na stronie, zatem czas niezbędny do dopasowania całego selektora będzie minimalny. Selektorem kluczowym dla drugiego wariantu będzie selektor uniwersalny. Ponieważ odpowiada on wszystkim elementom, przeglądarka musi sprawdzić każdy z nich, aby stwierdzić, czy jest on potomkiem odsyłacza z nazwą klasy class006
. Analiza każdego elementu strony powoduje największe spadki wydajności w selektorach, dlatego konstruowanie tego typu selektorów jest silnie odradzane.
Aktualny stan w przeglądarkach#
W dniu premiery artykuł Dave'a Hyatt'a był bardzo istotny z punktu widzenia deweloperów. W owym okresie implementacje selektorów CSS nie były dostatecznie zoptymalizowane, dlatego w przypadku dużej liczby elementów (dziesiątki tysięcy) i kosztownych selektorów mogliśmy doprowadzić do wyraźnego zadławienia przeglądarki. Czas jednak mijał, i jak się okazuje, sporo rzeczy ulepszono.
Zgodnie z artykułem Nicole Sullivan pod tytułem "CSS Selector Performance has changed! (For the better)" współczesne implementacje uległy znacznej optymalizacji. Cały opis powstał na bazie rozwoju silnika WebKit, za który zabrał się Antti Koivisto. Dzięki jego pracy problem wydajności wielu selektorów został zniwelowany do tego stopnia, że autorzy nie powinni się już tym zajmować. Podsumowaniem pracy programisty były słowa:
"Moim zdaniem autorzy nie powinni przejmować się optymalizacją selektorów (i z tego co widzę generalnie się nie przejmują), to powinno być zadaniem dla silnika."
Najlepiej samodzielnie przeanalizować artykuł pod kątem najnowszych rozwiązań WebKitu. Niestety, nie mam żadnych ciekawych informacji odnośnie pozostałych silników przeglądarek. Tak czy inaczej, według Anttiego, bezpośrednie i pośrednie sąsiedztwo kombinatorów wciąż może generować spadki wydajności, ale dzięki nowym optymalizacjom ich wpływ został znacznie zredukowany. Twierdzi on również, że istnieje jeszcze dużo miejsca dla optymalizacji pseudoklas i pseudoelementów. Trzeba jednak pamiętać, że cały proces i tak jest o wiele szybszy, niż próba uzyskania podobnych efektów za pomocą JavaScript i manipulacji DOM.
Testy#
Osoby zainteresowane tematem mogą pokusić się o samodzielne wykonanie praktycznych testów. W sieci znajduje się kilka wpisów poruszających kwestię wydajności selektorów, ale prezentowane tam wyniki dotyczą starszych przeglądarek (czasy IE6 i IE7). Ja proponuję testy Steve'a Soudersa; zawierają one kod HTML w postaci 1000 elementów a
zagnieżdżonych w strukturze DIV -> DIV -> DIV -> P
. Odnośnikom nadawane są style w następujący sposób:
- CSS Selectors: Baseline - 1000 reguł CSS, które nie wybierają żadnego elementu.
- CSS Selectors: Tag - 1 reguła CSS pasująca do wszystkich odsyłaczy w oparciu o selektor typu oraz 1000 reguł CSS bez zastosowania.
- CSS Selectors: Class - 1000 reguł CSS, z których każda wybiera konkretny odsyłacz w oparciu o selektor klasy (np.
.class0001
). - CSS Selectors: Child - 1000 reguł CSS, z których każda wybiera konkretny odsyłacz w oparciu o kombinator dziecka (np.
DIV > DIV > DIV > P > A.class0001
). - CSS Selectors: Descendant - 1000 reguł CSS, z których każda wybiera konkretny odsyłacz w oparciu o kombinator potomka (np.
DIV DIV DIV P A.class0001
). - CSS Selectors: Universal - 1000 reguł CSS, z których każda wybiera konkretny odsyłacz w oparciu o selektor uniwersalny (np.
P.pclass0001 *
). - CSS Test Creator - wygenerowanie własnego testu dla struktury HTML w postaci
DIV -> DIV -> DIV -> DIV -> DIV -> DIV (id='id0001') -> A (class='class0001')
.
Ze względu na wydajność aktualnych przeglądarek internetowych najwłaściwszym będzie skorzystanie z ostatniego sposobu. Generator pozwala wykreować nowy wariant testowy, z możliwością przekazania kilku dynamicznych parametrów (np. liczby elementów HTML lub reguł CSS). Dokładna instrukcja znajduje się w boksie umieszczonym w prawym górnym rogu strony. W przypadku testowania dużej liczby elementów i selektorów najlepiej zapisać wygenerowany test na dysku lokalnym.
Warto zwrócić uwagę na opcję Measure Reflow
dołączaną do każdego testu. Mierzy ona czas potrzebny na przeprowadzenie ponownego wlewania i przemalowania. Odpowiada za to następująca funkcja JS:
function measureReflow() {
document.body.style.display = 'none';
var startTime = Number(new Date());
document.body.style.display = '';
var bodyHeight = document.body.clientHeight;
var delta = Number(new Date()) - startTime;
document.getElementById('reflowdata').innerHTML += delta + " ";
}
Trzeba zdawać sobie sprawę z tego, że selektory (i ogólnie reguły CSS) mają pośredni wpływ na wydajność dynamicznych zmian wykonywanych przez JS i DOM. Jeśli selektory cechuje słaba wydajność, to odbije się to również na dynamicznych zmianach, ponieważ po każdej takiej zmianie arkusz CSS musi zostać przetworzony ponownie. Z moich testów wynika, że dla dużej liczby elementów najlepiej spisują się Chrome oraz Opera (nawet na Presto) osiągając czasy w przedziale 0-1 ms, gorzej jest z Firefoksem (szczególnie wersje z interfejsem Australis) oraz tradycyjnie już IE11 z najgorszy wynikiem rzędu 420 ms.
Chociaż wpływ selektorów na wydajność przetwarzania strony odgrywa coraz mniejsze znaczenie, to i tak można zetknąć się sytuacją, kiedy nieumiejętne utworzenie selektorów w połączeniu z dużą liczbą elementów może przydławić przeglądarkę. Dowodem tego będą trzy następujące testy:
- CSS Selectors: Baseline 10k - 10 tys. elementów bez żadnych stylów.
- CSS Selectors: ID 10k - dopasowanie 10 tys. elementów i reguł przy użyciu selektora identyfikatora (
#id
). - CSS Selectors: Descendant 10k - dopasowanie 10 tys. elementów i reguł przy użyciu kombinatora potomka (
DIV DIV DIV DIV DIV DIV A
).
Pierwszy z testów ma wykazać, że przetworzenie tak dużej liczby elementów, nawet bez stosowanie specjalnych stylów autora, generuje pewne koszta. Dwa kolejne testy obrazują przewagę najszybszego selektora identyfikatora nad najwolniejszym selektorem z wieloma kombinatorami potomka. Istotny jest trzeci przypadek, z którym najlepiej radzi sobie Chrome, po prostu deklasując rywali, co jedynie podkreśla fakt skutecznej optymalizacji selektorów, jaką przeprowadzono w tym programie na przestrzeni ostatnich lat. Pozostałe przeglądarki zawieszają interfejs na kilkanaście sekund. Warto o tym pamiętać i w razie kłopotów z płynnością interfejsu przyjrzeć się także swoim selektorom.