Dowiedz się, czym jest skaner wstępnego wczytywania w przeglądarce, jak pomaga on w zwiększaniu wydajności i jak możesz go uniknąć.
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 osiągnąć, 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.
Co to jest skaner wstępnego wczytania?
Każda przeglądarka ma podstawowy parsujący HTML, który znakuje znaczniki nieprzetworzone i przetwarza je w model obiektów. 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. Dlatego częstą praktyką jest wczytywanie kodu JavaScript na końcu dokumentu, aby efekty zablokowanego analizowania i renderowania były marginalne.
To wystarczające powody, dla których 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 robią, co w ich mocy, aby ograniczyć te problemy za pomocą dodatkowego analizatora HTML, zwanego skanerem wstępnego ładowania.
Rola skanera wstępnego jest spekulatywna, co oznacza, że skaner sprawdza nieprzetworzony 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. Jeśli te 2 problemy z wydajnością nigdy nie wystąpiły, skaner wstępnego wczytania nie będzie zbyt przydatny. Kluczem do określenia, czy strona internetowa korzysta ze skanera wstępnego, są te zjawiska blokowania. Aby to zrobić, możesz wprowadzić sztuczne opóźnienie dla żądań, aby dowiedzieć się, gdzie działa skaner wstępnego ładowania.
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ę, usługa proxy powoduje sztuczne opóźnienie o 2 sekundy dla arkusza stylów. 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ęte skrypty async
Załóżmy, że w sekcji <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 zostanie uruchomiony tak szybko, jak to możliwe, i nie zablokuje renderowania. Brzmi dobrze, prawda? Jeśli jednak założysz, że ten wbudowany element <script>
pojawia się po elemencie <link>
, który wczytuje zewnętrzny plik CSS, uzyskasz nieoptymalny wynik:
Oto, co się tutaj stało:
- W 0 sekundzie wysyłany jest dokument główny.
- W 1, 4 sekundy dociera pierwszy bajt żądania nawigacji.
- W 2, 0 sekundzie następuje żądanie pliku CSS i obrazu.
- 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 ładowania.
Co się stanie, jeśli zamiast wstrzykiwania skryptu do modelu DOM użyjesz zwykłego tagu <script>
z atrybutem async
?
<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 jest podwyższony, przeglądarka przydziela 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 za pomocą JavaScriptu
Łagodne wczytywanie to świetna metoda oszczędzania danych, która jest często stosowana w przypadku obrazów. Czasami jednak ładowanie opóźnione jest nieprawidłowo stosowane do obrazów, które są „powyżej zagięcia”.
Powoduje to potencjalne problemy z wykrywalnością zasobów w przypadku skanowania wstępnego i może niepotrzebnie wydłużać czas wykrywania odwołania do obrazu, jego pobierania, dekodowania i prezentowania. Weźmy na przykład ten znacznik obrazu:
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Użycie prefiksu data-
jest typowym wzorcem w opóźnionych ładowarkach opartych na 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 powoduje pobranie zasobu przez przeglądarkę.
Ten wzór nie stanowi problemu, dopóki nie zostanie zastosowany do obrazów, które znajdują się w widoku 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 z wyprzedzeniem pobrać zasobu obrazu(np. w momencie, w którym blokowanie renderowania przez arkusz stylów strony), wartość LCP się pogarsza.
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ć niewielką poprawą, ale jest to szybkie rozwiązanie, które wymaga zmiany znaczników, a większość stron internetowych jest bardziej skomplikowana niż te przykłady. 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, które mogą wymagać pobierania obrazów odwołujących się do właściwości background-image
.
Podobnie jak w przypadku HTML, przeglądarki przetwarzają kod CSS w swój własny model obiektów, zwany 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 usługi background-image
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">
Ten rel=preload
jest niewielki, ale pomaga przeglądarce szybciej wykryć obraz:
Dzięki podpowiedzi rel=preload
kandydat na element LCP jest odkrywany wcześniej, co skraca czas LCP. Ten podpowiedź może pomóc w rozwiązaniu tego problemu, ale lepszym rozwiązaniem może być ocena, czy obraz kandydujący do LCP musi być wczytany z CSS. Dzięki tagowi <img>
możesz lepiej kontrolować wczytywanie obrazu odpowiedniego dla widoku, a jednocześnie umożliwić skanowanie wstępne skanerom.
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. Jest ona widoczna w dokumencie i wczytuje się natychmiast. Ma on jednak istotne wady:
- Jeśli nie przechowujesz w pamięci podręcznej kodu HTML (a nie możesz tego zrobić, gdy odpowiedź HTML jest dynamiczna), zaszyte zasoby nigdy nie są przechowywane w pamięci podręcznej. Wpływa to na wydajność, ponieważ zaszyte zasoby nie mogą być używane wielokrotnie.
- Nawet jeśli możesz przechowywać w pamięci podręcznej kod HTML, wbudowane zasoby nie są udostępniane 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 wstawiasz zbyt wiele elementów, skanowanie wstępne opóźnia wykrywanie zasobów w dalszej części dokumentu, ponieważ pobieranie tego dodatkowego wstawionego elementu zajmuje więcej czasu.
Weźmy na przykład tę stronę. W pewnych warunkach kandydatem do LCP jest obraz u góry strony, a CSS znajduje się w oddzielnym pliku wczytywanym przez element <link>
. Strona korzysta też z 4 czcionek internetowych, które są żądane jako osobne pliki z zasobu 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 ramce, nie renderuje obrazu LCP do czasu, gdy upłynie ponad 7 sekund.
W tym przypadku chodzi o coś więcej niż tylko skaner wstępnego wczytania. Wstawianie czcionek w dokumentie nie jest dobrym rozwiązaniem, ponieważ format base64 jest nieefektywnym formatem 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 skracać czas LCP, ale nadmierne rozszerzanie kodu HTML, który nie może być przechowywany w pamięci podręcznej, za pomocą wstawionych zasobów ma inne negatywne konsekwencje dla wydajności. Ten wzór ma również wpływ na pierwsze wyrenderowanie treści (FCP). W wersji strony, w której nie ma żadnych wstawionych elementów, 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. Zwykle nie jest to zalecane, z wyjątkiem bardzo małych zasobów. Wstawiaj jak najmniej elementów, ponieważ umieszczanie zbyt wielu elementów w tekście może przynieść więcej szkody niż pożytku.
Renderowanie znaczników za pomocą kodu JavaScript po stronie klienta
Nie ma wątpliwości, że JavaScript ma wpływ na szybkość wczytywania strony. Deweloperzy polegają na nim nie tylko w zakresie interakcji, ale też w zakresie dostarczania treści. W pewnym sensie ułatwia to pracę deweloperom, ale korzyści dla deweloperów 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 przypadku tych przykładów żądanie obrazu LCP jest znacznie opóźnione w porównaniu z odpowiednim obrazem renderowanym na serwerze, który nie wymaga uruchamiania 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 wpływać 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 znacznik strony nie może być dostarczony przez serwer, a nie renderowany po stronie klienta? 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 poprzedzającym pobieraniu ważnych zasobów.
Jeśli Twoja strona musi używać JavaScriptu do dołączania funkcji do niektórych części znacznika strony, nadal możesz to robić za pomocą SSR, korzystając z tradycyjnego JavaScriptu lub hydratacji, aby połączyć zalety obu tych metod.
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 schematów, które uniemożliwiają wcześniejsze wykrywanie ważnych zasobów, nie tylko upraszcza proces tworzenia, ale też zapewnia lepsze wrażenia użytkowników, co przekłada się na lepsze wyniki w wielu rodzajach danych, w tym w przypadku niektórych wskaźników internetowych.
Oto podsumowanie 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 ładowania nie może wykryć zasobów, które nie są obecne w znacznikach dostarczonych przez serwer w początkowym żądaniu nawigacji. Skaner wstępnego wczytywania można obejść na różne sposoby, m.in.:
- 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 ładunku znaczników ze strony serwera.
- wczytywanie opóźnione obrazów i iframe’ów znajdujących się powyżej obszaru widocznego za pomocą rozwiązania JavaScript;
- Renderowanie znaczników po stronie klienta, które mogą zawierać odwołania do podzasobów 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 w tle, ponieważ jeśli wszystko będzie miało najwyższy priorytet, żadne z nich go nie będzie miało.
Zasoby
- Szkodliwe „skrypty asynchroniczne” 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 Unsplash autorstwa Mohammad Rahmani .