Dowiedz się, czym jest skaner wstępnego wczytywania przeglądarki, jak zwiększa jego wydajność i jak możesz uniknąć zakłóceń.
Jednym z pominiętych aspektów optymalizacji szybkości stron jest znajomość wewnętrznych funkcji przeglądarki. Przeglądarki wprowadzają pewne optymalizacje, aby poprawić wydajność w sposób, który nie jest możliwe dla programistów – ale tylko pod warunkiem, że nie uda im się w sposób nieumyślny wpłynąć na te optymalizacje.
Jednym z wewnętrznych funkcji optymalizacji przeglądarki jest skaner wstępnego wczytywania przeglądarki. W tym poście omówimy, jak działa skaner wstępnego wczytywania, a przede wszystkim dowiesz się, jak tego uniknąć.
Co to jest skaner wstępnego wczytywania?
Każda przeglądarka ma główny parser HTML, który tokenizuje nieprzetworzone znaczniki i przetwarza je w model obiektowy. Wszystko to dzieje się tak długo, aż parser się nie zatrzyma, gdy znajdzie zasób blokujący, np. arkusz stylów wczytany z elementem <link>
lub skrypt wczytany z elementem <script>
bez atrybutu async
lub defer
.
W przypadku plików CSS zarówno analizowanie, jak i renderowanie jest blokowane, aby uniknąć przebłysku niespójnej treści (FOUC), czyli sytuacji, gdy przed zastosowaniem stylów można zobaczyć wersję strony bez stylu.
Przeglądarka blokuje też analizowanie i renderowanie strony w przypadku napotkania elementów <script>
bez atrybutu defer
lub async
.
Dzieje się tak, ponieważ przeglądarka nie może stwierdzić, czy którykolwiek z skryptów zmieni model DOM, gdy podstawowy parser HTML będzie wykonywał swoje zadanie. Właśnie dlatego często ładuj kod JavaScript na końcu dokumentu, aby skutki zablokowanego analizowania i renderowania były minimalne.
Oto dobre powody, dla których przeglądarka powinna blokować zarówno analizowanie, jak i renderowanie. Jednak zablokowanie któregokolwiek z tych ważnych kroków jest niepożądane, ponieważ może je wstrzymać, opóźniając znalezienie innych ważnych zasobów. Na szczęście te przeglądarki starają się radzić sobie z tym problemem dzięki dodatkowemu parserowi HTML, zwanemu skanerem wstępnego wczytywania.
Rola skanera wstępnego jest spekulacyjna, co oznacza, że sprawdza nieprzetworzone znaczniki w celu znalezienia zasobów do pobrania oportunistycznego, zanim główny parser HTML je wykryje.
Jak sprawdzić, czy skaner wstępnego wczytywania działa
Skaner wstępnego wczytywania istnieje z powodu zablokowania renderowania i analizowania. Gdyby te 2 problemy z wydajnością nie występowały, skaner wstępnego wczytywania nie byłby zbyt przydatny. Kluczem do ustalenia, czy skaner wstępnego wczytywania strony będzie używany, zależy od tych zjawisk blokujących. W tym celu możesz wprowadzić sztuczne opóźnienie żądań informacji o tym, gdzie działa skaner wstępnego wczytywania.
Weźmy jako przykład tę stronę zawierającą tekst i obrazy z arkuszem stylów. Ponieważ pliki CSS blokują zarówno renderowanie, jak i analizowanie, użytkownik wprowadza sztuczne opóźnienie dla arkusza stylów wynoszące dwa sekundy za pośrednictwem usługi proxy. Dzięki temu łatwiej jest zobaczyć w kaskadzie sieci, gdzie działa skaner wstępnego wczytywania.
Jak widać w kaskadzie, skaner wstępnego wczytywania wykrywa element <img>
nawet wtedy, gdy renderowanie i analiza dokumentu jest zablokowane. Bez tej optymalizacji przeglądarka nie może w trakcie okresu blokowania pobierać treści w osobisty sposób, a więcej żądań zasobów odbywałoby się kolejno po sobie, a nie równocześnie.
Teraz spójrzmy na przykłady rzeczywistych wzorców, w których można zapobiec skanerowi wstępnego wczytywania, a także co można zrobić, by te problemy rozwiązać.
Wstrzyknięto async
skryptu
Załóżmy, że w sekcji <head>
masz kod HTML, który zawiera wbudowany kod JavaScript, taki jak:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
Wstrzykiwane skrypty mają wartość domyślną async
, więc po wstawieniu skryptu działa on tak, jakby został do niego zastosowany atrybut async
. Oznacza to, że zacznie on działać jak najszybciej i nie będzie blokować renderowania. Brzmi optymalnie, prawda? Jeśli jednak przyjmiesz, że wbudowany element <script>
znajduje się po elemencie <link>
, który wczytuje zewnętrzny plik CSS, otrzymasz nieoptymalny wynik:
Przeanalizujmy, co się wydarzyło:
- W 0 sekundach jest wysyłane żądanie dokumentu głównego.
- Po 1, 4 sekundy przychodzi pierwszy bajt żądania nawigacji.
- Po 2 sekundzie wymagane jest żądanie CSS i obrazu.
- Ponieważ parser blokuje wczytywanie arkusza stylów, a wbudowany skrypt JavaScript, który wstrzykuje skrypt
async
, pojawia się po 2,6 sekundy tego arkusza stylów, dlatego funkcje zapewniane przez skrypt nie są dostępne tak szybko, jak by mogły.
Jest to nieoptymalne, ponieważ żądanie skryptu jest wysyłane dopiero po zakończeniu pobierania arkusza stylów. Spowoduje to jak najszybsze uruchomienie skryptu. Element <img>
jest natomiast wykrywalny w znacznikach dostarczanych przez serwer, dlatego zostaje wykryty przez skaner wstępnego wczytywania.
Co się więc stanie, jeśli użyjesz zwykłego tagu <script>
z atrybutem async
zamiast wstrzyknięcia skryptu w DOM?
<script src="/yall.min.js" async></script>
Wynik to:
Może się wydawać, że te problemy można rozwiązać przy użyciu rel=preload
. Z pewnością to zadziała, ale może mieć pewne skutki uboczne. W końcu po co używać rel=preload
do rozwiązywania problemu, którego można uniknąć, nie wstawiając elementu <script>
w DOM?
Wstępne wczytywanie tego problemu „rozwiązuje” ten problem, ale wprowadza nowy problem: skrypt async
w dwóch pierwszych wersjach demonstracyjnych – pomimo wczytania w środowisku <head>
– jest wczytywany z priorytetem „Niski”, a arkusz stylów jest wczytywany z priorytetem „Najwyższym”. W ostatniej wersji demonstracyjnej, w której został wstępnie wczytany skrypt async
, arkusz stylów był nadal wczytywany z priorytetem „Najwyższy”, ale został on podniesiony do poziomu „Wysoki”.
Gdy priorytet zasobu zostanie podniesiony, przeglądarka przypisze mu większą przepustowość. Oznacza to, że nawet jeśli arkusz stylów ma najwyższy priorytet, podniesiony priorytet skryptu może powodować rywalizację z przepustowością. Może to być przyczyną powolnego połączenia lub dość dużych zasobów.
Odpowiedź jest prosta: jeśli podczas uruchamiania potrzebny jest skrypt, nie należy godzić się na skanowanie skanera, wstawiając go w modelu DOM. W razie potrzeby poeksperymentuj z rozmieszczeniem elementów typu <script>
oraz z atrybutami takimi jak defer
i async
.
Leniwe ładowanie z użyciem JavaScriptu
Leniwe ładowanie to świetna metoda oszczędzania danych, często stosowana do obrazów. Czasami leniwe ładowanie jest jednak nieprawidłowo stosowane do obrazów, które znajdują się w części strony widocznej na ekranie.
Może to stwarzać potencjalne problemy z wykrywaniem zasobów ze względu na skaner wstępnego wczytywania. Może też niepotrzebnie opóźniać czas potrzebny na wykrycie odniesienia do obrazu, pobranie go, zdekodowanie i zaprezentowanie. Przeanalizujmy ten przykład znacznika obrazu:
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Zastosowanie prefiksu data-
jest częstym wzorcem w leniwych programach ładowania obsługiwanych przez JavaScript. Gdy obraz zostanie przewinięty do widocznego obszaru, leniwe ładowanie usuwa prefiks data-
, co oznacza, że w poprzednim przykładzie data-src
zmienia się w src
. Ta aktualizacja zachęca przeglądarkę do pobrania zasobu.
Ten wzorzec nie stanowi problemów, dopóki nie zostanie zastosowany do obrazów, które podczas uruchamiania znajdują się w widocznym obszarze. Skaner wstępnego wczytywania nie odczytuje atrybutu data-src
w taki sam sposób jak atrybut src
(lub srcset
), więc odwołanie do zdjęcia nie zostało wykryte wcześniej. Co gorsza, wczytywanie obrazu jest opóźnione do po pobraniu, skompilowaniu i wykonaniu kodu JavaScript leniwego ładowania.
W zależności od rozmiaru obrazu, który może zależeć od wielkości widocznego obszaru, może on też stanowić element kandydujący do największego wyrenderowania treści (LCP). Gdy skaner wstępnego wczytywania nie może spekulacyjnie pobrać zasobu graficznego z wyprzedzeniem, na przykład w czasie, gdy arkusz stylów strony blokuje renderowanie, ucierpi LCP.
Rozwiązaniem jest zmiana znaczników obrazu:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Jest to optymalny wzorzec w przypadku obrazów znajdujących się w widocznym obszarze podczas uruchamiania, ponieważ skaner wstępnego wczytywania będzie szybciej wykrywać i pobierać zasoby graficzne.
Wynikiem tego uproszczonego przykładu jest poprawa LCP o 100 milisekund w przypadku powolnego połączenia. Może to nie wydawać się wielkim usprawnieniem, ale gdy weźmiemy pod uwagę szybkie poprawki w znacznikach i większość stron internetowych jest bardziej skomplikowana niż ten zestaw przykładów. Oznacza to, że kandydaci do LCP mogą mieć trudności z rywalizowaniem o przepustowość z wieloma innymi zasobami, więc takie optymalizacje stają się coraz ważniejsze.
Obrazy tła CSS
Pamiętaj, że skaner wstępnego wczytywania przeglądarki skanuje znaczniki. Nie skanuje innych typów zasobów, takich jak CSS, co może wiązać się z pobraniem obrazów, do których odwołuje się właściwość background-image
.
Podobnie jak w przypadku HTML, przeglądarki przetwarzają kod CSS we własnym modelu obiektowym o nazwie CSSOM. Jeśli podczas tworzenia CSSOM zostaną wykryte zasoby zewnętrzne, są one żądane w momencie wykrycia, a nie przez skaner wstępnego wczytywania.
Załóżmy, że kandydat LCP Twojej strony to element z właściwością CSS background-image
. Oto, co dzieje się podczas wczytywania zasobów:
W takim przypadku skaner wstępnego wczytywania nie jest tak bardzo zagrożony, jak i niezaangażowany. Nawet jeśli potencjalny LCP na stronie pochodzi z właściwości CSS background-image
, należy wstępnie wczytywać ten obraz:
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
Ta wskazówka dotycząca atrybutu rel=preload
jest niewielka, ale pomaga przeglądarce wykryć obraz szybciej, niż w innym przypadku:
Dzięki wskazówce rel=preload
kandydat LCP jest odkrywany wcześniej, co skraca czas LCP. Ta wskazówka pomaga rozwiązać ten problem, ale lepszą opcją może być sprawdzenie, czy kandydujący obraz LCP został wczytany z CSS. Tag <img>
daje większą kontrolę nad wczytywaniem obrazu, który pasuje do widocznego obszaru, a jednocześnie umożliwia wykrywanie tego obrazu przez skaner.
Wstawiono zbyt wiele zasobów
Wbudowanie polega na umieszczaniu zasobu w kodzie HTML. Możesz używać arkuszy stylów w elementach <style>
, skryptów w elementach <script>
i w praktycznie dowolnych innych zasobach, używając kodowania base64.
Wbudowanie zasobów może być szybsze niż ich pobranie, ponieważ dla danego zasobu nie jest wysyłane osobne żądanie. Znajdują się one bezpośrednio w dokumencie i są natychmiast wczytywane. Istnieją jednak poważne wady:
- Jeśli nie umieszczasz kodu HTML w pamięci podręcznej – a nie możesz, jeśli odpowiedź HTML jest dynamiczna – zasoby wbudowane nie są przechowywane w pamięci podręcznej. Ma to wpływ na wydajność, ponieważ wbudowanych zasobów nie można używać ponownie.
- Nawet jeśli możesz buforować kod HTML, wbudowane zasoby nie są udostępniane między dokumentami. Zmniejsza to wydajność buforowania w porównaniu z plikami zewnętrznymi, które można przechowywać w pamięci podręcznej i ponownie używać w całym źródle.
- Jeśli umieścisz je w tekście, opóźnisz skanowanie przed odszukaniem przez skaner zasobów w dalszej części dokumentu, ponieważ pobieranie dodatkowych treści w tekście trwa dłużej.
Dla przykładu wybierz tę stronę. W określonych warunkach propozycja LCP to obraz u góry strony, a kod CSS znajduje się w osobnym pliku wczytywanym przez element <link>
. Strona wykorzystuje także 4 czcionki internetowe, które są żądane jako osobne pliki od zasobu CSS.
Co się teraz stanie, jeśli CSS i wszystkie czcionki będą wbudowane jako zasoby base64?
W tym przykładzie wpływ w trakcie w reklamie wpływa negatywnie na LCP, a także na ogólną skuteczność. Wersja strony, która nie zawiera żadnych elementów w treści, tworzy obraz LCP w ciągu około 3,5 sekundy. Strona, która zawiera wszystko, maluje obraz LCP dopiero po nieco ponad 7 sekundach.
To coś więcej niż tylko skaner wstępnego wczytywania. Wbudowane czcionki nie są dobrym rozwiązaniem, ponieważ base64 nie jest skutecznym formatem dla zasobów binarnych. Innym czynnikiem jest to, że zewnętrzne zasoby czcionek nie są pobierane, chyba że CSSOM uzna je za konieczne. Jeśli czcionki te są wbudowane w standard base64, zostaną pobrane niezależnie od tego, czy są potrzebne na bieżącej stronie.
Czy wstępne wczytywanie może tu coś poprawić? Jasne. Możesz wstępnie wczytać obraz LCP i skrócić czas LCP, ale nadmierne wyświetlanie kodu HTML, którego nie można przechowywać w pamięci podręcznej, może mieć inne negatywne konsekwencje związane z wydajnością. Ten wzorzec wpływa również na pierwsze wyrenderowanie treści (FCP). W wersji strony, w której nic nie jest wbudowane, FCP wynosi około 2,7 sekundy. W wersji, w której wszystko jest w tekście, FCP wynosi około 5,8 sekundy.
Zachowaj szczególną ostrożność przy włączaniu treści do kodu HTML, zwłaszcza zasobów zakodowanych w formacie base64. Ogólnie nie jest to zalecane z wyjątkiem bardzo małych zasobów. Umieść w tekście jak najmniejszą treść, bo za dużo w treści idzie ogień.
Renderowanie znaczników za pomocą kodu JavaScript po stronie klienta
Co do tego nie ma wątpliwości: JavaScript z pewnością wpływa na szybkość strony. Deweloperzy nie tylko polegają na nim w zapewnieniu interaktywności, ale są też dość częste, by polegać na niej w celu dostarczania treści. Poprawia to w pewnym stopniu komfort deweloperów, ale korzyści nie zawsze przekładają się na korzyści dla użytkowników.
Jednym ze wzorców, które mogą poradzić sobie ze skanerem wstępnego wczytywania, jest renderowanie znaczników za pomocą JavaScriptu po stronie klienta:
Gdy ładunki znaczników są zawarte i renderowane w całości przez JavaScript w przeglądarce, wszystkie zasoby w tych znacznikach są w praktyce niewidoczne dla skanera wstępnego wczytywania. Opóźnia to wykrywanie ważnych zasobów, co z pewnością wpływa na LCP. W takich przypadkach żądanie obrazu LCP jest znacznie opóźnione w porównaniu z odpowiednim procesem renderowanym przez serwer, który nie wymaga JavaScriptu.
Trochę odbiega to od tematyki tego artykułu, ale wpływ renderowania znaczników na klienta znacznie wykracza poza walkę ze skanerem wstępnego wczytywania. Po pierwsze, wprowadzenie JavaScriptu do obsługi środowiska, które nie wymaga jego obsługi, wydłuża czas przetwarzania, który może negatywnie wpływać na interakcję z kolejnym wyrenderowaniem (INP).
Poza tym renderowanie bardzo dużych ilości znaczników na kliencie zwiększa prawdopodobieństwo wygenerowania długich zadań w porównaniu z taką samą ilością znaczników wysyłanych przez serwer. Dzieje się tak, oprócz dodatkowego przetwarzania związanego z JavaScriptem, ponieważ przeglądarki przesyłają znaczniki z serwera i dzielą renderowanie w taki sposób, aby uniknąć długich zadań. Znaczniki renderowane przez klienta są natomiast obsługiwane jako jedno monolityczne zadanie, które oprócz wartości INP może wpływać na dane o czasie reagowania stron, takie jak całkowity czas blokowania (TBT) lub opóźnienie przy pierwszym działaniu (FID).
Rozwiązanie problemu w takim przypadku zależy od odpowiedzi na pytanie: Czy istnieje jakiś powód, dla którego serwer nie może dostarczyć znaczników strony, zamiast renderować ją po stronie klienta? Jeśli odpowiedź na to pytanie brzmi „nie”, w miarę możliwości należy wziąć pod uwagę renderowanie po stronie serwera (SSR) lub znaczniki generowane statycznie, ponieważ pomoże to skanerowi w wykryciu i opportunistycznym pobrać ważne zasoby z wyprzedzeniem.
Jeśli Twoja strona wymaga JavaScriptu, aby dołączyć funkcje do niektórych części znaczników, możesz to zrobić za pomocą SSR za pomocą waniliowego JavaScriptu lub nawodnienia, aby wykorzystać zalety obu formatów.
Skaner wstępnego wczytywania może Ci pomóc
Skaner wstępnego wczytywania to wysoce skuteczna optymalizacja przeglądarki, która pomaga szybciej wczytywać strony podczas uruchamiania. Dzięki unikaniu wzorców, które uniemożliwiają mu dotarcie do ważnych zasobów z wyprzedzeniem, nie tylko ułatwiasz sobie programowanie, ale także zapewniasz lepsze wrażenia użytkowników, co z kolei przekłada się na lepsze wyniki w zakresie wielu danych, w tym niektórych podstawowych wskaźników internetowych.
Oto kilka rzeczy, o których warto pamiętać w tym poście:
- Skaner wstępnego wczytywania przeglądarki to dodatkowy parser HTML, który skanuje przed serwerem głównym, jeśli jest zablokowany w celu znalezienia zasobów, które może pobrać szybciej.
- Skaner wstępnego wczytywania nie może wykryć zasobów, których nie ma w znacznikach dostarczonych przez serwer w ramach wstępnego żądania nawigacji. Problemy ze skanerem wstępnego wczytywania mogą obejmować między innymi:
- Wstawianie w modelu DOM zasobów, takich jak skrypty, obrazy, arkusze stylów czy inne elementy, które warto umieścić we wstępnym ładunku znaczników pochodzącym z serwera.
- Leniwe ładowanie obrazów w części strony widocznej na ekranie lub elementów iframe przy użyciu rozwiązania JavaScript.
- Renderowanie na kliencie znaczników, które mogą zawierać odwołania do zasobów podrzędnych dokumentów, przy użyciu JavaScriptu.
- Skaner wstępnego wczytywania skanuje tylko kod HTML. Nie sprawdza zawartości innych zasobów – w szczególności CSS – które mogą zawierać odwołania do ważnych zasobów, w tym do kandydatów LCP.
Jeśli z jakiegokolwiek powodu nie możesz uniknąć wzorca, który negatywnie wpływa na zdolność skanera do przyspieszania wczytywania strony, skorzystaj z wskazówek dotyczących zasobów rel=preload
. Jeśli używasz rel=preload
, przetestuj je w narzędziach laboratoryjnych, aby się upewnić, że przynosi to oczekiwane rezultaty. Nie wczytuj wstępnie zbyt wielu zasobów, ponieważ gdy ustalisz priorytety, nic się nie uda.
Zasoby
- Szkodliwe „skrypty asynchroniczne” wstawiane przez skrypt
- Jak moduł wstępnego ładowania przeglądarki przyspiesza wczytywanie stron
- Wstępne wczytywanie najważniejszych zasobów w celu przyspieszenia wczytywania
- Nawiązuj połączenia sieciowe wcześniej, aby poprawić postrzeganą szybkość stron
- Optymalizowanie największego wyrenderowania treści
Baner powitalny ze zbioru Unsplash, autor: Mohammad Rahmani .