Dowiedz się, czym jest skaner wstępnego wczytywania w przeglądarce, jak zwiększa wydajność i jak zabezpieczyć się przed utratą ruchu.
Jednym z pomijanych aspektów optymalizacji szybkości strony jest znajomość wewnętrznej budowy przeglądarki. Przeglądarki wprowadzają pewne optymalizacje, aby poprawić wydajność w sposób, którego my, jako deweloperzy, nie możemy wykorzystać, ale tylko wtedy, gdy te optymalizacje nie zostaną przypadkowo zablokowane.
Jedną z optymalizacji przeglądarki wewnętrznej, którą warto poznać, jest skaner wstępnego ładowania przeglądarki. Z tego artykułu dowiesz się, jak działa skaner wstępnego wczytywania i jak uniknąć problemów z jego działaniem.
Czym jest skaner wstępnego wczytywania?
Każda przeglądarka ma podstawowy parser HTML, który tokenizuje nieprzetworzone znaczniki i przetwarza je w modelu obiektowym. Wszystko to trwa do momentu, gdy parsowanie zostanie wstrzymane po znalezieniu zasobu blokującego, takiego jak wczytana szata wczytana za pomocą elementu <link>
lub skrypt wczytany za pomocą elementu <script>
bez atrybutu async
lub defer
.
W przypadku plików CSS renderowanie jest blokowane, aby zapobiec błyskawicznemu wyświetlaniu treści bez stylów (FOUC), czyli sytuacji, w której na krótko widoczna jest wersja strony bez stylów, zanim zostaną one zastosowane.
Przeglądarka blokuje też analizowanie i renderowanie strony, gdy napotyka elementy <script>
bez atrybutu defer
lub async
.
Dzieje się tak, ponieważ przeglądarka nie może mieć pewności, czy dany skrypt zmodyfikuje DOM, gdy główny parsarz HTML nadal wykonuje swoje zadanie. Właśnie dlatego tak powszechną praktyką jest ładowanie kodu JavaScript na końcu dokumentu, tak by skutki zablokowania analizy i renderowania były marginalne.
Z tych powodów przeglądarka powinna blokować zarówno analizowanie, jak i renderowanie. Blokowanie któregokolwiek z tych ważnych kroków jest jednak niepożądane, ponieważ może opóźnić odkrywanie innych ważnych zasobów. Na szczęście przeglądarki starają się rozwiązać te problemy, korzystając z dodatkowego parsera HTML nazywanego skanerem wstępnego wczytywania.
Rola skanera wstępnego jest spekulacyjna, co oznacza, że skaner sprawdza surowy znacznik, aby znaleźć zasoby do pobrania, zanim główny parsujący HTML je odkryje.
Jak sprawdzić, czy skaner wstępnego wczytania działa
Skaner wstępnego wczytywania istnieje z powodu zablokowanego renderowania i analizowania. Gdyby te 2 problemy z wydajnością nie występowały, skaner wstępnego wczytywania nie byłby przydatny. Kryterium tych zjawisk blokujących jest kluczowe dla określenia, czy użycie skanera wstępnego wczytywania jest korzystne dla strony. Aby to zrobić, możesz wprowadzić sztuczne opóźnienie w odpowiedzi na żądania sprawdzenia, gdzie działa skaner wstępnego wczytywania.
Weźmy na przykład tę stronę z tekstem i obrazami z wykorzystaniem arkusza stylów. Ponieważ pliki CSS blokują zarówno renderowanie, jak i analizę, wprowadzasz sztuczne opóźnienie o 2 sekundy dla arkusza stylów za pomocą usługi proxy. Dzięki temu łatwiej jest sprawdzić w splacie sieciowym, kiedy działa skaner wstępnego ładowania.
Jak widać na wykresie kaskadowym, skaner wstępnego załadowania wykrywa element <img>
nawet wtedy, gdy renderowanie i analiza dokumentu są zablokowane. Bez tej optymalizacji przeglądarka nie może pobierać danych w okresie blokowania, a więcej żądań zasobów będzie się pojawiać kolejno, a nie równocześnie.
Po przykładzie zabawki przyjrzyjmy się teraz prawdziwym wzorom, w których przypadku skaner wstępnego ładowania może zostać pokonany, oraz temu, jak można to naprawić.
Wstrzyknięto async
skryptu
Załóżmy, że w pliku <head>
masz kod HTML, który zawiera kod JavaScript wbudowany w tekst, np. taki:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
Wstrzyknięte skrypty są domyślnie async
, więc po wstrzyknięciu skrypt będzie zachowywać się tak, jakby miał zastosowany atrybut async
. Oznacza to, że będzie ona uruchamiana jak najszybciej i nie będzie blokowała renderowania. Brzmi optymalnie, prawda? Jeśli jednak przy założeniu, że ten wbudowany <script>
występuje po elemencie <link>
, który wczytuje zewnętrzny plik CSS, otrzymasz nieoptymalny wynik:
Opiszmy to, co się tutaj wydarzyło:
- W 0 sekundzie wysyłany jest dokument główny.
- W 1,4 sekundy dociera pierwszy bajt żądania nawigacji.
- W 2, 0 sekundy są wysyłane żądania CSS i obraz.
- Ponieważ parsowanie jest zablokowane, wczytywanie arkusza stylów i wbudowany kod JavaScript, który wstrzykuje kod
async
, pojawia się po tym arkuszu stylów po 2,6 sekundy, funkcja tego kodu nie jest dostępna od razu.
Nie jest to optymalne rozwiązanie, ponieważ żądanie skryptu jest wysyłane dopiero po zakończeniu pobierania arkusza stylów. Dzięki temu skrypt nie zostanie uruchomiony od razu. Z drugiej strony, element <img>
jest dostępny w znacznikach dostarczonych przez serwer, więc jest wykrywany przez skaner wstępnego wczytania.
Co się więc stanie, gdy użyjesz zwykłego tagu <script>
z atrybutem async
, a nie wstawić skrypt do DOM?
<script src="/yall.min.js" async></script>
Oto wynik:
Możesz mieć pokusę, by zasugerować, że te problemy można rozwiązać, używając rel=preload
. To na pewno zadziała, ale może mieć pewne skutki uboczne. Po co używać elementu rel=preload
do rozwiązania problemu, którego można uniknąć, nie wstrzykując elementu <script>
do DOM?
Wczytywanie z wyprzedzeniem „rozwiązuje” ten problem, ale powoduje pojawienie się nowego: skrypt async
w pierwszych 2 demonstracjach – mimo że jest wczytywany w ramach <head>
– jest wczytywany z priorytetem „Niski”, podczas gdy arkusz stylów jest wczytywany z priorytetem „Najwyższy”. W ostatnim pokazie, w którym skrypt async
jest wstępnie wczytany, wczytywanie arkusza stylów nadal ma priorytet „Najwyższy”, ale priorytet skryptu został podwyższony do „Wysoki”.
Gdy priorytet zasobu wzrośnie, przeglądarka przydzieli mu więcej przepustowości. Oznacza to, że nawet jeśli szata ma najwyższy priorytet, podwyższony priorytet skryptu może spowodować konflikt przepustowości. Może to być spowodowane wolnym połączeniem lub dużymi zasobami.
Odpowiedź jest prosta: jeśli skrypt jest potrzebny podczas uruchamiania, nie pokonuj skanera wstępnego ładowania, wstrzykując go do DOM. W razie potrzeby eksperymentuj z umieszczaniem elementów <script>
oraz z atrybutami takimi jak defer
i async
.
Leniwe ładowanie z użyciem JavaScriptu
Łagodne wczytywanie to świetna metoda oszczędzania danych, która jest często stosowana w przypadku obrazów. Czasami jednak leniwe ładowanie jest nieprawidłowo stosowane do obrazów w części strony widocznej na ekranie.
Stwarza to potencjalne problemy z wykrywaniem zasobów w przypadku skanera wstępnego ładowania i może niepotrzebnie opóźniać znalezienie odniesienia do obrazu, pobranie go, zdekodowanie i prezentowanie. Weźmy na przykład te znaczniki obrazu:
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Prefiks data-
jest często używany przez leniwe ładowanie oparte na języku JavaScript. Gdy obraz zostanie przewinięty do widoku, opóźniony ładowacz usunie prefiks data-
, co oznacza, że w poprzednim przykładzie data-src
stanie się src
. Ta aktualizacja spowoduje, że przeglądarka pobierze zasób.
Ten wzorzec nie stanowi problemu, dopóki nie zostanie zastosowany do obrazów, które znajdują się w widocznym obszarze podczas uruchamiania. Skaner wstępnego wczytania nie odczytuje atrybutu data-src
w taki sam sposób jak atrybutu src
(lub srcset
), więc odniesienie do obrazu nie zostanie wcześniej wykryte. Co gorsza, obraz jest wczytywany dopiero po pobraniu, skompilowaniu i wykonywaniu kodu JavaScript.
W zależności od rozmiaru obrazu, który może zależeć od rozmiaru widocznego obszaru, może to być element kwalifikujący się do największego wyrenderowania treści (LCP). Gdy skaner wstępnego wczytywania nie może spekulacyjnie pobrać zasobu obrazu z wyprzedzeniem – na przykład w momencie, w którym doszło do renderowania blokowego arkuszy stylów strony – na tym korzystnie wpływa ten wskaźnik.
Rozwiązaniem jest zmiana znaczników obrazu:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Jest to optymalny wzór dla obrazów, które są widoczne w widoku podczas uruchamiania, ponieważ skaner wstępnego ładowania szybciej wykryje i pobierze zasób obrazów.
W tym uproszczonym przykładzie wynik to poprawa LCP o 100 ms przy wolnym połączeniu. Może się to wydawać niezwykłe, ale okazuje się, że rozwiązaniem jest szybka poprawka znaczników, a większość stron internetowych jest bardziej złożona niż ten zestaw przykładów. Oznacza to, że kandydaci do LCP mogą konkurować o przepustowość z wielkimi zasobami, dlatego takie optymalizacje stają się coraz ważniejsze.
Obrazy tła w kodzie CSS
Pamiętaj, że skaner wstępnego załadowania przeglądarki skanuje oznaczenia. Nie skanuje innych typów zasobów, takich jak CSS, co może wymagać pobierania obrazów, do których odwołuje się właściwość background-image
.
Podobnie jak HTML, przeglądarki przetwarzają kod CSS na własny model obiektowy, nazywany CSSOM. Jeśli podczas tworzenia obiektu CSSOM zostaną wykryte zasoby zewnętrzne, są one żądane w momencie ich wykrycia, a nie przez skaner wstępnego ładowania.
Załóżmy, że kandydat na LCP na stronie to element z właściwością CSS background-image
. Podczas wczytywania zasobów:
W takim przypadku skaner wstępnego ładowania nie został tak naprawdę pokonany, tylko nie był używany. Mimo to, jeśli kandydat na element LCP na stronie pochodzi z elementu background-image
usługi CSS, warto go wstępnie załadować:
<!-- 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 rel=preload
jest krótka, ale pomaga przeglądarce wykryć obraz szybciej niż w innym przypadku:
Wskazówka rel=preload
sprawia, że kandydat LCP jest wykrywany szybciej, co skraca czas LCP. Ta wskazówka pomaga rozwiązać ten problem, ale lepszą opcją może być sprawdzenie, czy kandydat LCP obrazu musi zostać załadowany z CSS. Dzięki tagowi <img>
możesz lepiej kontrolować wczytywanie obrazu odpowiedniego dla widoku, a jednocześnie umożliwić skanowanie go przez skaner wstępnego wczytywania.
Wstawianie zbyt wielu zasobów
Wstawianie to umieszczanie zasobu w kodzie HTML. Możesz wstawiać w treści style w elementach <style>
, skrypty w elementach <script>
i praktycznie dowolne inne zasoby za pomocą kodowania base64.
Wstawianie zasobów może być szybsze niż ich pobieranie, ponieważ nie jest wysyłane osobne żądanie. Znajdziesz ją w dokumencie i błyskawicznie wczytywana. Ma on jednak istotne wady:
- Jeśli nie umieszczasz kodu HTML w pamięci podręcznej (i nie możesz tego zrobić, jeśli odpowiedź HTML jest dynamiczna), wbudowane zasoby nigdy nie są zapisywane w pamięci podręcznej. Wpływa to na wydajność, ponieważ zaszyte zasoby nie mogą być użyte ponownie.
- Nawet jeśli możesz zapisać kod HTML w pamięci podręcznej, zasoby wbudowane nie są współdzielone między dokumentami. Ogranicza to skuteczność pamięci podręcznej 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 wstawisz zbyt wiele treści, skanowanie wstępne opóźni się, ponieważ wykrywanie zasobów w dalszej części dokumentu zajmie więcej czasu.
Dla przykładu przyjrzyj się tej stronie. W pewnych warunkach kandydatem do LCP jest obraz u góry strony, a plik CSS znajduje się w oddzielnym pliku wczytywanym przez element <link>
. Strona korzysta również z czterech czcionek internetowych, które są żądane jako osobne pliki z zasobem CSS.
Co się stanie, jeśli CSS iwszystkie czcionki są wbudowane jako zasoby base64?
W tym przykładzie wstawienie kodu inline negatywnie wpływa na LCP, a także ogólną wydajność. Wersja strony, która nie zawiera żadnych elementów wstawianych, renderuje obraz LCP w około 3,5 sekundy. Strona, która wyświetla wszystko w linii, nie renderuje obrazu LCP do ponad 7 sekund.
Nie chodzi tylko o skaner wstępnego wczytywania. Wbudowanie czcionek nie jest dobrym rozwiązaniem, ponieważ base64 nie jest wydajnym formatem dla zasobów binarnych. Kolejnym czynnikiem jest to, że zewnętrzne zasoby czcionek nie są pobierane, chyba że zostaną uznane za niezbędne przez CSSOM. Gdy te czcionki są wstawiane w formacie base64, są pobierane niezależnie od tego, czy są potrzebne na bieżącej stronie.
Czy w takim przypadku załadowanie wstępne może pomóc? Jasne. Możesz wstępnie wczytać obraz LCP i skrócić czas LCP, ale przesłanie kodu HTML, którego nie można zapisać w pamięci podręcznej, za pomocą wbudowanych zasobów ma inne negatywne konsekwencje związane z wydajnością. Ten wzorzec ma również wpływ na pierwsze wyrenderowanie treści (FCP). W wersji strony, na której nic nie znajduje się w tekście, wskaźnik FCP wynosi około 2,7 sekundy. W wersji, w której wszystko jest wstawione, FCP wynosi około 5,8 sekund.
Bardzo ostrożnie podchodź do wstawiania elementów 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. Wstawiaj jak najmniej elementów, ponieważ zbyt duża liczba wstawionych elementów może spowodować problemy.
Renderowanie znaczników za pomocą kodu JavaScript po stronie klienta
Nie ma co do tego wątpliwości: JavaScript ma zdecydowanie wpływ na szybkość działania strony. Deweloperzy nie tylko polegają na niej, aby zapewnić interaktywność, ale też na samo dostarczanie treści. W pewnym sensie sprzyja to deweloperom, ale korzyści dla nich nie zawsze przekładają się na korzyści dla użytkowników.
Jednym z wzorów, który może uniemożliwić działanie skanera wstępnego wczytywania, jest renderowanie znaczników za pomocą kodu JavaScript po stronie klienta:
Gdy dane znaczników są zawarte w JavaScript i w pełni renderowane przez niego w przeglądarce, wszystkie zasoby w tym znaczniku są w praktyce niewidoczne dla skanera wstępnego. Opóźnia to wykrywanie ważnych zasobów, co z pewnością wpływa na LCP. W tych przykładach żądanie obrazu LCP jest znacznie opóźnione w porównaniu z odpowiednim renderowaniem przez serwer, które nie wymaga JavaScriptu.
To trochę odbiega od tematu tego artykułu, ale efekty renderowania znaczników na kliencie wykraczają daleko poza pokonywaniem skanera wstępnego ładowania. Po pierwsze, wprowadzenie JavaScriptu do obsługi funkcji, która go nie wymaga, powoduje niepotrzebne przetwarzanie, co może mieć wpływ na czas od interakcji do kolejnego wyrenderowania (INP). Renderowanie bardzo dużych ilości znaczników na kliencie powoduje dłuższe zadania niż w przypadku takiej samej ilości znaczników wysyłanych przez serwer. Powodem tego, poza dodatkowym przetwarzaniem wymaganym przez JavaScript, jest to, że przeglądarki przesyłają znaczniki ze serwera i dzielą renderowanie na części w taki sposób, aby ograniczać czas wykonywania długich zadań. Z drugiej strony znaczniki renderowane po stronie klienta są obsługiwane jako pojedyncze, monolityczne zadanie, które może wpływać na INP strony.
Rozwiązanie tego problemu zależy od odpowiedzi na to pytanie: Czy istnieje powód, dla którego serwer nie może zapewnić znaczników strony zamiast renderowania na kliencie? Jeśli odpowiedź na to pytanie brzmi „nie”, należy rozważyć renderowanie po stronie serwera (SSR) lub generowanie znaczników statycznych, o ile to możliwe, ponieważ pomoże to skanerowi w poprzednim wykrywaniu i pobieraniu ważnych zasobów.
Jeśli Twoja strona wymaga JavaScriptu, by dodać funkcje do niektórych części znaczników strony, nadal możesz to zrobić za pomocą SSR – za pomocą JavaScriptu waniliowego lub hydratacji. Pozwoli Ci to korzystać z obu rozwiązań.
Jak korzystać z skanera wstępnego
Skaner wstępnego wczytywania to bardzo skuteczna optymalizacja przeglądarki, która pomaga szybciej wczytywać strony podczas uruchamiania. Unikanie wzorców, które uniemożliwiałyby wykrywanie ważnych zasobów z wyprzedzeniem, nie tylko ułatwiasz sobie programowanie, ale także zapewniasz użytkownikom lepsze wrażenia, co przekłada się na lepsze wyniki pod względem wielu wskaźników, w tym niektórych wskaźników internetowych.
Oto najważniejsze informacje z tego posta:
- Skaner wstępnego wczytywania przeglądarki to dodatkowy parsujący HTML, który skanuje przed głównym, jeśli jest zablokowany, aby optymistycznie wykrywać zasoby, które może pobrać wcześniej.
- Skaner wstępnego wczytywania nie wykrywa zasobów, których nie ma w znacznikach podanych przez serwer w pierwszym żądaniu nawigacji. Skaner wstępnego ładowania może zostać zniwelowany między innymi przez:
- Wstrzykiwanie zasobów do DOM za pomocą JavaScriptu, np. skryptów, obrazów, arkuszy stylów lub innych elementów, które lepiej byłoby umieścić w pierwotnym obciążeniu znaczników ze strony serwera.
- wczytywanie z opóźnieniem obrazów i iframe znajdujących się powyżej pola widzenia za pomocą rozwiązania JavaScript;
- Znaczniki renderowania po stronie klienta, które mogą zawierać odwołania do zasobów podrzędnych dokumentu za pomocą JavaScriptu.
- Skaner wstępnego załadowania skanuje tylko kod HTML. Nie sprawdza zawartości innych zasobów, zwłaszcza CSS, które mogą zawierać odniesienia do ważnych zasobów, w tym kandydatów do LCP.
Jeśli z jakiegokolwiek powodu nie możesz uniknąć wzoru, który negatywnie wpływa na zdolność skanera wstępnego wczytywania do przyspieszania wczytywania, skorzystaj z wskazówek dotyczących zasobów rel=preload
. Jeśli używasz narzędzia rel=preload
, przetestuj je w narzędziach laboratoryjnych, aby upewnić się, że działają zgodnie z oczekiwaniami. Nie wczytuj też zbyt wielu zasobów, ponieważ jeśli wszystko będzie miało najwyższy priorytet, żadne z nich nie będzie miało priorytetu.
Zasoby
- Szkodliwe „skrypty niesynchroniczne” wstawiane przez skrypt
- Jak wstępny wczytywanie w przeglądarce przyspiesza wczytywanie stron
- Przelewanie kluczowych komponentów w celu zwiększenia szybkości wczytywania
- Wczesna konfiguracja połączeń sieciowych w celu poprawy postrzeganej szybkości strony
- Optymalizacja największego wyrenderowania treści
Baner powitalny z filmu Unsplash, którego autorem jest Mohammad Rahmani .