Między wciśnięciem Enter a gotową stroną na ekranie przeglądarka wykonuje kilkanaście kroków: odnajduje serwer, pobiera pliki, buduje strukturę strony, oblicza układ, rysuje piksele i uruchamia skrypty. Każdy z tych kroków może być wąskim gardłem, a przy audytach wydajności zawsze zaczynam od ustalenia, na którym z nich strona traci czas. W tym wpisie przechodzę przez cały proces po kolei, prosto, ale bez uproszczeń, i przy każdym etapie pokazuję, co w praktyce najczęściej go spowalnia i co z tym robię.
Odnalezienie serwera i połączenie
Przeglądarka zna nazwę domeny, ale łączy się z adresem IP. Najpierw pyta więc system DNS, pod jakim adresem działa serwer. Odpowiedź często jest już zapamiętana, ale przy pierwszej wizycie zapytanie może przejść przez kilka serwerów. Potem nawiązuje połączenie z serwerem, a przy HTTPS dodatkowo uzgadnia szyfrowanie. Przy HTTP/3 część tych kroków odbywa się jednocześnie, dzięki czemu połączenie powstaje szybciej.
W praktyce ten etap trwa długo, gdy serwer stoi daleko od użytkownika, gdy strona pobiera zasoby z wielu różnych domen, z których
każda wymaga osobnego połączenia, albo gdy DNS jest wolny lub źle skonfigurowany. Pomaga CDN, ograniczenie liczby domen zewnętrznych oraz
preconnect dla tych, bez których strona się nie obejdzie. Przy zmianach serwera zwracam też uwagę na
konfigurację rekordów DNS, bo błędy w niej wydłużają lub psują ten etap.
Odpowiedź serwera
Serwer otrzymuje żądanie i przygotowuje odpowiedź. Przy stronie statycznej po prostu wysyła plik. Przy WordPressie czy sklepie uruchamia aplikację, która wykonuje zapytania do bazy danych i składa HTML. Czas od wysłania żądania do otrzymania pierwszego bajtu odpowiedzi to TTFB.
W praktyce wysoki TTFB to prawie zawsze ciężka aplikacja, wolne zapytania do bazy albo za słaby serwer. Najwięcej daje pamięć podręczna całych stron, a przy stronach generowanych dynamicznie poprawa samej aplikacji. Przy kilkuset milisekundach TTFB żadna optymalizacja zdjęć czy CSS nie zrekompensuje straty, dlatego zawsze sprawdzam go jako pierwszy, a przy wyborze serwera biorę pod uwagę to, jaki hosting pasuje do projektu.
Budowanie struktury strony
Przeglądarka czyta HTML od góry i na bieżąco buduje z niego drzewo elementów, czyli DOM. Gdy trafia na arkusz stylów, pobiera go i buduje
osobną strukturę reguł CSS. Gdy trafia na zwykły znacznik <script>, zatrzymuje czytanie HTML, pobiera skrypt i wykonuje go,
bo skrypt może zmienić stronę. Jednocześnie przegląda dalszą część dokumentu i z wyprzedzeniem zaczyna pobierać zdjęcia, style i skrypty,
które tam znajdzie.
W praktyce ten etap spowalniają dwie rzeczy. Pierwsza to skrypty w sekcji <head> bez atrybutów
defer lub async, które wstrzymują budowanie strony. Druga to zasoby, których przeglądarka nie widzi w HTML, np. zdjęcie
w tle ustawione w CSS albo treść dodawana przez JavaScript. Odkrywa je dopiero później, więc pobiera z opóźnieniem. Ogromne drzewo elementów
wydłuża z kolei każdy kolejny etap, dlatego przy rozbudowanych szablonach staram się
zmniejszyć rozmiar DOM.
Od struktury do pierwszego obrazu
Przeglądarka nie wyświetli strony, dopóki nie ma kompletu stylów potrzebnych do jej narysowania, bo bez nich musiałaby pokazać nieostylowaną treść, a za chwilę ją przebudować. Gdy ma już strukturę HTML i style, łączy je, oblicza położenie i wymiary każdego elementu, czyli układ, a następnie rysuje piksele: tekst, kolory, obramowania i zdjęcia. Moment pojawienia się pierwszej treści to FCP, a największego elementu na ekranie, np. zdjęcia w nagłówku, to LCP.
W praktyce pierwszy obraz opóźniają duże pliki CSS, z których strona używa niewielkiej części, czcionki ukrywające tekst do czasu pobrania oraz zdjęcie w nagłówku pobierane zbyt późno lub zbyt ciężkie. Rozwiązania to wydzielenie stylów potrzebnych na pierwszym ekranie, czyli refaktoryzacja CSS pod kątem FCP, poprawne ładowanie czcionek i nadanie priorytetu zdjęciu, które decyduje o wyniku, jaki daje optymalizacja LCP.
Warstwy i płynność
Przeglądarka nie rysuje strony jako jednego obrazu. Część elementów, np. animowane menu czy przyklejony nagłówek, umieszcza na osobnych warstwach, które karta graficzna składa w gotowy obraz. Dzięki temu przy przewijaniu czy animacji przesunięcia i przezroczystości nie trzeba przeliczać układu ani rysować wszystkiego od nowa.
W praktyce płynność psują animacje zmieniające szerokość, wysokość czy położenie elementów, bo zmuszają przeglądarkę do
przeliczania układu w każdej klatce, oraz elementy, które zmieniają wymiary po załadowaniu i przesuwają resztę strony. Animuję więc głównie
transform i opacity, a zdjęciom, banerom i osadzonym treściom rezerwuję miejsce z góry, co poprawia wskaźnik CLS
w Core Web Vitals.
JavaScript i reakcja na działania użytkownika
Skrypty, obsługa kliknięć, przeliczanie stylów i rysowanie dzielą jeden główny wątek przeglądarki. Gdy skrypt wykonuje się długo, przeglądarka w tym czasie nie zareaguje na kliknięcie ani nie narysuje nowej klatki. Strona wygląda na gotową, ale menu otwiera się z opóźnieniem, a pole wyszukiwania przyjmuje znaki ze zwłoką. W aplikacjach renderowanych na serwerze dochodzi do tego hydratacja, czyli ponowne uruchomienie komponentów w przeglądarce, które też zajmuje główny wątek.
W praktyce najczęstszymi winowajcami są skrypty zewnętrzne, page buildery i duże paczki JavaScriptu ładowane na każdej podstronie. Mechanizm, przez który blokują stronę, wyjaśnia Event Loop w JavaScript, a sposoby skracania długich zadań to temat, jakim zajmuje się refaktoryzacja JavaScript pod kątem INP. Sposób, w jaki strona powstaje, czyli w przeglądarce, na serwerze czy podczas budowania, ma tu ogromne znaczenie, a wybór między nimi ułatwia porównanie metod renderowania CSR, SSR, SSG i ISR.
Kolejne wizyty
Przy powrocie na stronę przeglądarka może pominąć część kroków. Adres serwera zna już z pamięci DNS, a pliki CSS, JavaScript, czcionki i zdjęcia może wziąć z pamięci podręcznej, jeśli serwer na to pozwolił. Wtedy pobiera tylko HTML i to, co się zmieniło.
W praktyce wiele stron traci tę korzyść przez brak lub złe ustawienie nagłówków pamięci podręcznej, przez co przeglądarka pobiera wszystko od nowa przy każdej wizycie. Dlatego przy każdym projekcie ustawiam nagłówki Cache-Control: długie przechowywanie dla plików statycznych z wersją w nazwie i krótkie dla HTML.
Od czego zaczynam, gdy strona jest wolna
Gdy znam kolejność tych etapów, poszukiwanie przyczyny jest proste: sprawdzam je po kolei, od serwera do JavaScriptu, i zatrzymuję się na tym, który zajmuje najwięcej czasu. Najczęściej wygląda to tak:
- TTFB powyżej ok. 0,8 s: najpierw serwer, aplikacja i pamięć podręczna stron,
- długi czas do pierwszej treści: style i skrypty blokujące oraz czcionki,
- długi czas do LCP przy szybkim FCP: zdjęcie w nagłówku, jego priorytet, format i rozmiar,
- przesunięcia układu: brakujące wymiary zdjęć i elementy doładowywane nad treścią,
- opóźniona reakcja na kliknięcia: długie zadania JavaScriptu i skrypty zewnętrzne.
Wszystkie te etapy widać na osi czasu w narzędziach deweloperskich przeglądarki, w zakładkach Network i Performance. Dzięki temu poprawiam to, co rzeczywiście spowalnia stronę, a nie to, co akurat podpowiada ogólny raport.