Dowiedz się, czym jest skaner wstępnego wczytywania w przeglądarce i w jaki sposób zwiększa wydajność, oraz dowiedz się, jak się przed nim chronić.
Jednym z pomijanych aspektów optymalizacji szybkości strony jest znajomość wewnętrznej budowy przeglądarki. Przeglądarki przeprowadzają pewne optymalizacje poprawiające wydajność w sposób, którego my jako programiści nie możemy tego robić, ale tylko pod warunkiem, że optymalizacje nie będą nieumyślnie zakłócane.
Jedną z wewnętrznych metod optymalizacji, która pozwala zrozumieć działanie przeglądarek, jest skaner wstępnego wczytywania. Z tego posta dowiesz się, jak działa skaner wstępnego wczytywania i przede wszystkim, jak możesz go uniknąć.
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 przebiega płynnie, dopóki parser nie zatrzyma się, gdy znajdzie zasób blokujący, np. arkusz stylów załadowany z elementem <link>
lub skrypt załadowany z elementem <script>
bez atrybutu async
lub defer
.
W przypadku plików CSS renderowanie jest blokowane, aby zapobiec przebłysku niesformatowanej treści (FOUC), czyli sytuacji, gdy niesformatowana wersja strony może być krótko widoczna, zanim zostaną do niej zastosowane style.
Przeglądarka blokuje również analizowanie i renderowanie strony, jeśli natrafi na elementy <script>
bez atrybutu defer
lub async
.
Powodem jest to, że przeglądarka nie ma pewności, czy dany skrypt będzie modyfikować DOM, podczas gdy podstawowy parser HTML nadal wykonuje swoje zadanie. Dlatego zwykło się wczytywać kod JavaScript na końcu dokumentu, aby efekty zablokowanego analizowania 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 ładowania ma charakter spekulacyjny, co oznacza, że analizuje nieprzetworzone znaczniki w celu znalezienia zasobów do pobrania, zanim podstawowy parser HTML wykryje je w innym przypadku.
Jak sprawdzić, czy skaner wstępnego wczytywania działa
Skaner wstępnego wczytywania istnieje ze względu na zablokowane renderowanie i analizę. 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 dla żądań, aby dowiedzieć się, gdzie działa skaner wstępnego ładowania.
Dla przykładu przyjrzyj się tej stronie zawierającej podstawowy tekst i obrazy z arkuszem stylów. Pliki CSS blokują renderowanie i analizę, dlatego wprowadzasz sztuczne opóźnienie arkusza stylów o 2 sekundy za pośrednictwem usługi pośredniczącej. Dzięki temu opóźnienie w kaskadzie sieci jest łatwiejsze, w którym działa skaner wstępnego wczytywania.
Jak widać w kaskadzie, skaner wstępnego wczytywania wykrywa element <img>
, nawet jeśli renderowanie i analiza dokumentów są zablokowane. Bez tej optymalizacji przeglądarka nie jest w stanie pobierać dodatkowych elementów w okresie blokowania, a więcej żądań zasobów byłoby następujących po sobie, a nie równocześnie.
Skoro już omówiliśmy ten przykład z zabawkami, spójrzmy na pewne realistyczne wzorce, w których skaner wstępnie ładowanego systemu może się skończyć oraz co można zrobić, by rozwiązać ten problem.
Wstrzyknięto async
skryptu
Załóżmy, że w pliku <head>
masz kod HTML zawierający wbudowany JavaScript, taki jak:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
Domyślnie wstrzyknięty skrypt ma wartość async
, więc po wstrzykiwaniu działa tak, jakby został do niego 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:
Oto, co się tutaj stało:
- W 0 sekundzie wysyłany jest dokument główny.
- Po 1, 4 sekundy pojawia się 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, ponieważ żądanie skryptu następuje dopiero po zakończeniu pobierania arkusza stylów. To spowoduje opóźnienie wykonania skryptu tak szybko, jak to możliwe. Element <img>
jest natomiast wykrywalny w znacznikach dostarczanych przez serwer. Dlatego skaner wstępnego wczytywania wykrywa go.
Co się stanie, jeśli zamiast wstrzykiwania skryptu do DOM-u użyjesz zwykłego tagu <script>
z atrybutem async
?
<script src="/yall.min.js" async></script>
Oto rezultat:
Możesz mieć pokusę, by zasugerować, że te problemy można rozwiązać, używając rel=preload
. To oczywiście zadziała, ale może wiązać się z pewnymi skutkami ubocznymi. Po co więc używać rel=preload
do rozwiązania problemu, którego można uniknąć, nie wstrzykiując elementu <script>
do DOM?
Wstępne wczytywanie „poprawek” tutaj pojawia się nowy problem, ale pojawia się nowy problem: skrypt async
w 2 pierwszych wersjach demonstracyjnych (mimo załadowania w <head>
) jest wczytywany „Niski” a arkusz stylów ma wartość „Najwyższa” . W ostatniej wersji demonstracyjnej, w której skrypt async
jest wstępnie wczytywany, arkusz stylów jest nadal wczytywany na poziomie „Najwyższa” ale priorytet skryptu został zwiększony do „Wysoki”.
Gdy priorytet zasobu wzrośnie, przeglądarka przydzieli mu więcej przepustowości. Oznacza to, że chociaż arkusz stylów ma najwyższy priorytet, zwiększony priorytet skryptu może powodować rywalizację o przepustowość. Może to powodować powolne połączenia lub bardzo duże zasoby.
Odpowiedź jest prosta: jeśli podczas uruchamiania potrzebny jest skrypt, nie pokonuj skanera wstępnego wczytywania, wstrzykijąc go do DOM. W razie potrzeby poeksperymentuj z umiejscowieniem elementu <script>
oraz takimi atrybutami jak defer
i async
.
Leniwe ładowanie za pomocą JavaScriptu
Leniwe ładowanie to świetna metoda zachowania danych, którą często stosuje się do obrazów. Czasami jednak leniwe ładowanie jest nieprawidłowo stosowane do obrazów w części strony widocznej na ekranie.
Może to powodować potencjalne problemy z wykrywalnością zasobów w przypadku skanera wstępnego ładowania i może niepotrzebnie wydłużać czas potrzebny na znalezienie odwołania do obrazu, jego pobranie, dekodowanie i wyświetlenie. Weźmy na przykład ten znacznik 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 znajdzie się w widocznym obszarze, leniwe ładowanie usuwa prefiks data-
, co oznacza, że w poprzednim przykładzie data-src
zmienia się w 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 wczytywania nie odczytuje atrybutu data-src
w taki sam sposób jak w atrybutach src
(lub srcset
), dlatego odwołanie do obrazu nie zostało wykryte wcześniej. Co gorsza, obraz może się wczytać dopiero po pobraniu, skompilowaniu i wykonaniu kodu JavaScript przez leniwe ładowanie.
W zależności od rozmiaru obrazu – który może zależeć od rozmiaru widocznego obszaru – może on być elementem kandydującym 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">
To optymalny wzorzec dla obrazów, które znajdują się w widocznym obszarze podczas uruchamiania, ponieważ skaner wstępnego wczytywania szybciej wykrywa i pobiera zasób obrazu.
W wyniku tego uproszczonego przykładu mamy do czynienia z poprawką wskaźnika LCP o 100 milisekund w przypadku wolnego połączenia. 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 LCP być może będą musieli konkurować o przepustowość z wieloma innymi zasobami, a takie optymalizacje stają się coraz ważniejsze.
Obrazy tła CSS
Pamiętaj, że skaner wstępnie wczytuje znaczniki w przeglądarce. 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, nazywany CSSOM. Jeśli zasoby zewnętrzne zostaną wykryte podczas tworzenia CSSOM, są one żądane w momencie wykrycia, a nie przez skaner wstępnego wczytywania.
Załóżmy, że kandydat LCP na Twojej stronie jest elementem 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 zaniedbany, jak żaden. Mimo to, jeśli kandydat LCP na stronie pochodzi z właściwości CSS background-image
, warto wstępnie wczytać 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 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. Tag <img>
daje Ci większą kontrolę nad ładowaniem obrazu odpowiedniego do widocznego obszaru, a jednocześnie umożliwia wykrywanie tego przez skaner wstępnie załadowanego.
Wbudowano zbyt wiele zasobów
Wbudowanie to metoda polegająca na umieszczaniu zasobu w kodzie HTML. Możesz wstawiać arkusze stylów w elementach <style>
, skrypty w elementach <script>
, a także praktycznie w dowolnych innych zasobach za pomocą kodowania base64.
Wbudowane zasoby mogą być szybsze niż ich pobieranie, ponieważ dla zasobu 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ż wbudowanych zasobów nie nadają się do wielokrotnego użytku.
- Nawet jeśli możesz przechowywać w pamięci podręcznej 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 wykorzystywać w całym punkcie początkowym.
- Jeśli wstawiasz zbyt wiele elementów, skanowanie wstępne opóźni się, ponieważ wykrywanie zasobów w dalszej części dokumentu będzie trwało dłużej.
Dla przykładu przyjrzyj się tej stronie. W pewnych warunkach kandydat LCP jest obrazem na górze strony, a plik CSS znajduje się w osobnym pliku wczytywanym przez element <link>
. Strona używa też 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 ma w tekście żadnego elementu wbudowanego, obrazuje obraz LCP w ciągu ok.3,5 sekundy. Strona, która wyświetla wszystko w ramce, nie renderuje obrazu LCP do ponad 7 sekund.
W tym przypadku chodzi o coś więcej niż tylko skaner wstępnego wczytania. 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. Czcionki te są pobierane w standardzie base64 i niezależnie od tego, czy są potrzebne na bieżącej stronie.
Czy wstępne załadowanie może coś tu poprawić? 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 w tekście, FCP wynosi około 5,8 sekundy.
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. Wbudowane jak najmniej, ponieważ za dużo w treści to ingerencje w świat ognia.
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 nie tylko polegają na niej, aby zapewnić interaktywność, ale też na samo dostarczanie treści. Pod wieloma względami zapewnia to deweloperom lepsze wrażenia. ale korzyści dla programistów nie zawsze przekładają się na korzyści dla użytkowników.
Jednym z wzorców, który może pokonać skaner wstępnego wczytywania, jest renderowanie znaczników za pomocą JavaScriptu po stronie klienta:
Gdy ładunki znaczników są w przeglądarce zawarte i w pełni renderowane przez JavaScript, wszelkie zawarte w nich zasoby 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 tych przykładach żądanie obrazu LCP jest znacznie opóźnione w porównaniu z odpowiednim renderowaniem przez serwer, które nie wymaga JavaScriptu.
Ten artykuł odbiega od tematu tego artykułu, ale wpływ renderowania znaczników na klienta jest znacznie bardziej skomplikowany niż skaner wstępnego wczytywania. Po pierwsze, wprowadzenie JavaScriptu w celu zwiększenia wydajności, które nie wymaga więcej czasu, wiąże się z wprowadzaniem niepotrzebnego czasu przetwarzania, który może mieć wpływ na interakcję z następnym wyrenderowaniem (INP). Renderowanie bardzo dużych ilości znaczników na kliencie daje większe prawdopodobieństwo wygenerowania długich zadań niż tyle samo znaczników wysyłanych przez serwer. Powodem, oprócz dodatkowego przetwarzania, który wymaga JavaScript, jest to, że przeglądarki przesyłają strumieniowo znaczniki z serwera i dzielą renderowanie w sposób, który zazwyczaj ogranicza długie zadania. Znaczniki renderowane przez klienta są natomiast obsługiwane jako pojedyncze, monolityczne zadanie, które może wpływać na wartość INP strony.
Rozwiązanie tego problemu zależy od odpowiedzi na to pytanie: Czy istnieje powód, dla którego znaczniki strony nie mogą być dostarczane przez serwer, a zamiast tego są renderowane po stronie klienta? Jeśli odpowiedź brzmi „nie”, w miarę możliwości warto wziąć pod uwagę renderowanie po stronie serwera (SSR) lub statycznie wygenerowane znaczniki. Pomoże to skanerowi wstępnego wczytywania i oszczędnego pobrania ważnych zasobów z wyprzedzeniem.
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 to wykorzystać oba te elementy.
Jak korzystać z skanera wstępnego
Skaner wstępnego wczytywania to bardzo skuteczna optymalizacja przeglądarki, która przyspiesza wczytywanie stron podczas uruchamiania. Unikając wzorców, które uniemożliwiałyby poznanie 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 rodzajów danych, w tym niektórych wskaźników internetowych.
Oto podsumowanie tego posta:
- Skaner wstępnego wczytywania przeglądarki to dodatkowy parser HTML, który skanuje przed nadrzędnym, jeśli jest zablokowany, aby umożliwić wyszukiwanie zasobów, 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 załadowania może być ukryty między innymi z następujących powodów:
- 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.
- Leniwe ładowanie obrazów w części strony widocznej na ekranie lub elementów iframe 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ąć wzorca, który negatywnie wpływa na zdolność skanera wstępnego wczytywania, zastosuj wskazówkę dotyczącą 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 ładuj wstępnie zbyt wielu zasobów, bo jeśli ustalisz priorytety dla wszystkich, nic się nie stanie.
Zasoby
- „Skrypty niesynchroniczne” wstawiane przez skrypt uznawane za szkodliwe
- Jak moduł wstępnego ładowania przeglądarki przyspiesza wczytywanie stron
- Wstępnie wczytuj najważniejsze zasoby, aby przyspieszyć wczytywanie
- Nawiąż połączenie sieciowe, aby poprawić postrzeganą szybkość działania stron
- Optymalizowanie największego wyrenderowania treści
Baner powitalny z filmu Unsplash, autor: Mohammad Rahmani .