Przy audytach wydajności coraz częściej trafiam na strony, które ładują się przyzwoicie, a mimo to działają ociężale: przewijanie się zacina, menu otwiera się z opóźnieniem, a kliknięcie w filtr reaguje po chwili. Jedną z najczęstszych przyczyn jest zbyt rozbudowane drzewo DOM, czyli tysiące elementów HTML, z których większość nie jest użytkownikowi do niczego potrzebna. W tym wpisie pokazuję, jak mierzę rozmiar DOM, skąd biorą się zbędne elementy i jak je ograniczam w motywach, page builderach i aplikacjach JavaScript.
Dlaczego rozmiar DOM ma znaczenie
Każdy element na stronie to praca dla przeglądarki. Przy każdej zmianie, np. otwarciu menu czy dodaniu produktu do koszyka, musi ona ponownie dopasować style i przeliczyć układ części strony. Im więcej elementów i im głębiej są zagnieżdżone, tym dłużej to trwa i tym więcej pamięci zużywa strona. Najbardziej odczuwają to użytkownicy słabszych telefonów, a w danych widać to jako gorszy wynik Interaction to Next Paint, czyli opóźnioną reakcję na kliknięcia.
Rozmiar DOM wpływa też na skrypty. Każde wyszukanie elementów, każda pętla po węzłach i każda aktualizacja interfejsu w frameworku trwają dłużej, gdy drzewo jest ogromne. To element szerszego procesu, jakim jest działanie strony internetowej w przeglądarce: przeglądarka musi zbudować drzewo, zanim cokolwiek wyświetli, i przeliczać je przy każdej zmianie.
Jak mierzę rozmiar DOM
Najszybciej w konsoli narzędzi deweloperskich na otwartej stronie:
// liczba wszystkich elementów
document.querySelectorAll('*').length
// największa głębokość zagnieżdżenia
Math.max(...[...document.querySelectorAll('*')].map(el => {
let depth = 0;
while (el.parentElement) { el = el.parentElement; depth++; }
return depth;
}))
Lighthouse i PageSpeed Insights pokazują ten sam problem w raporcie: łączną liczbę elementów, największą głębokość i element z największą liczbą bezpośrednich potomków. Ostrzeżenie pojawia się już przy kilkuset elementach, a wyraźny problem przy ponad 1400. Traktuję te liczby jako punkt orientacyjny, nie cel sam w sobie. Ważniejsze jest, czy strona reaguje płynnie przy realnym użyciu, co sprawdzam w zakładce Performance, klikając w menu, filtry i formularze.
Przy dużych serwisach sprawdzam rozmiar DOM dla każdego typu szablonu osobno: strony głównej, kategorii, produktu, wpisu. Zwykle problem dotyczy jednego lub dwóch szablonów, a nie całej strony.
Skąd biorą się tysiące zbędnych elementów
| Przyczyna | Jak to wygląda w kodzie | Co robię |
|---|---|---|
| Page buildery | Każdy blok to kilka zagnieżdżonych kontenerów: sekcja, kolumna, wrapper, widżet | Przejście na nowsze, płaskie kontenery oparte na Flexboxie, upraszczanie układów |
| Menu w dwóch wersjach | Pełne menu desktopowe i osobne menu mobilne, oba w kodzie każdej strony | Jedno menu stylowane dla obu widoków |
| Mega menu | Setki linków do wszystkich podkategorii w kodzie każdej podstrony | Ograniczenie liczby linków, doładowanie zawartości przy pierwszym otwarciu |
| Ukryte treści | Wysuwane panele, okna modalne, zakładki i slidery obecne w kodzie, choć niewidoczne | Renderowanie dopiero przy otwarciu lub ograniczenie liczby slajdów |
| Ikony SVG wklejane wielokrotnie | Ta sama ikona jako pełny kod SVG przy każdym produkcie | Sprite SVG i odwołania przez <use> |
| Długie listy | Setki produktów, komentarzy czy wierszy tabeli na jednej stronie | Paginacja, „Pokaż więcej” lub wirtualizacja listy |
| Widżety zewnętrzne | Czaty, opinie, kanały z mediów społecznościowych dokładające własne drzewa elementów | Ładowanie po interakcji lub rezygnacja z części widżetów |
Jak upraszczam strukturę w motywach i page builderach
Najwięcej daje zmiana podejścia do układu. Stare techniki wymagały dodatkowych kontenerów do wyrównania i odstępów. Dziś CSS Grid
i Flexbox pozwalają uzyskać ten sam efekt na jednym elemencie nadrzędnym, bez zagnieżdżania kolejnych div-ów. Przy
przebudowie szablonów w ten sposób zwykle wyraźnie zmniejszam liczbę elementów, bez żadnej zmiany w wyglądzie.
W page builderach, takich jak Elementor, przechodzę ze starych sekcji i kolumn na nowsze kontenery, które generują znacznie płytszą strukturę. Usuwam też widżety używane tylko do odstępów, puste kolumny i bloki ukryte na urządzeniach mobilnych, bo ukrycie w CSS nie usuwa ich z kodu. Często okazuje się, że największy problem nie leży w samym builderze, tylko w nadmiarze dodatków i efektów, czyli klasycznym overengineeringu.
Długie listy i treści poniżej pierwszego ekranu
Przy listach produktów nie wyświetlam setek pozycji naraz. Zamiast tego stosuję paginację albo przycisk „Pokaż więcej” z zachowaniem adresów kolejnych stron. W aplikacjach z bardzo długimi listami, np. tabelami w panelu, używam wirtualizacji, czyli renderowania tylko tych wierszy, które są aktualnie widoczne.
Dla rozbudowanych sekcji poniżej pierwszego ekranu pomocna jest właściwość CSS content-visibility: auto. Przeglądarka pomija wtedy
układanie i rysowanie sekcji, dopóki użytkownik do niej nie przewinie. Elementy zostają w drzewie, więc treść jest dostępna dla wyszukiwarek
i czytników ekranu, ale strona szybciej się wyświetla. Zawsze ustawiam przy tym szacowaną wysokość sekcji przez
contain-intrinsic-size, żeby uniknąć przeskakiwania paska przewijania.
DOM w aplikacjach JavaScript
W aplikacjach opartych na frameworkach problemem jest nie tylko liczba elementów, ale też to, jak często się zmieniają. Kilka zasad, których pilnuję:
- zmiany w drzewie wprowadzam hurtowo, np. budując fragment w pamięci i wstawiając go raz, zamiast dodawać elementy po jednym w pętli,
- nie odczytuję wymiarów elementów zaraz po zmianie stylów w tej samej pętli, bo wymusza to wielokrotne przeliczanie układu,
- komponenty niewidoczne, np. zamknięte okna modalne, odmontowuję zamiast tylko je ukrywać,
- nie renderuję tej samej treści dwa razy dla różnych urządzeń, tylko dopasowuję ją stylami.
Po każdej zmianie porównuję liczbę elementów i nagranie z zakładki Performance przed poprawką i po niej. Zmniejszenie DOM o połowę rzadko zmienia wygląd strony, ale prawie zawsze zmienia to, jak szybko reaguje na użytkownika.