Optymalizacja wczytywania zasobów

W poprzednim module omówiliśmy pewną teorię dotyczącą krytycznej ścieżki renderowania i dowiedzieliśmy się, jak zasoby blokujące renderowanie i blokujące parsery mogą opóźnić wstępne renderowanie strony. Skoro już znasz część teorii, możesz więc zapoznać się z technikami optymalizacji krytycznej ścieżki renderowania.

Gdy strona się wczytuje, w jej kodzie HTML odwołuje się wiele zasobów, które zapewniają jej wygląd i układ (za pomocą CSS), a jej interaktywność dzięki JavaScriptowi. W tym module omówimy kilka ważnych pojęć związanych z tymi zasobami i ich wpływ na czas wczytywania strony.

Blokowanie renderowania

Jak wspomnieliśmy w poprzednim module, CSS jest zasobem blokujący renderowanie, ponieważ blokuje on przeglądarce możliwość renderowania jakichkolwiek treści do czasu utworzenia obiektowego modelu CSSOM. Przeglądarka blokuje renderowanie, aby zapobiec wprowadzeniu treści bez stylu (FOUC), co jest niepożądane z punktu widzenia użytkownika.

W poprzednim filmie widać krótkie pole FOUC, które pozwala zobaczyć stronę bez żadnych stylów. Następnie wszystkie style są stosowane, gdy kod CSS strony zakończy wczytywanie z sieci, a wersja bez stylu strony jest natychmiast zastępowana wersją ze stylem.

Ogólnie rzecz biorąc, błąd FOUC to coś, co się zwykle nie wyświetla, ale warto zrozumieć pojęcie tej koncepcji. Dzięki temu dowiesz się, dlaczego przeglądarka blokuje renderowanie strony do czasu pobrania kodu CSS i zastosowania na niej kodu. Blokowanie renderowania nie zawsze jest niepożądane, ale na pewno chcesz zminimalizować czas jego trwania, optymalizując CSS.

Blokowanie parsera

Zasób blokujący parser przerywa działanie parsera HTML, np. element <script> bez atrybutów async lub defer. Gdy parser napotka element <script>, przed rozpoczęciem analizy pozostałej części kodu HTML przeglądarka musi ocenić i wykonać skrypt. To zaprojektowane z myślą o zaprojektowaniu, ponieważ skrypty w trakcie jego tworzenia mogą modyfikować DOM lub uzyskiwać do niego dostęp.

<!-- This is a parser-blocking script: -->
<script src="/script.js"></script>

W przypadku korzystania z zewnętrznych plików JavaScript (bez elementów async i defer) parser jest blokowany w momencie wykrycia pliku do momentu jego pobrania, analizy i wykonania. Gdy używany jest wbudowany kod JavaScript, parser jest blokowany w podobny sposób do czasu analizy i wykonania wbudowanego skryptu.

Skaner wstępnego wczytywania

Skaner wstępnego wczytywania to optymalizacja przeglądarki w postaci dodatkowego parsera HTML, który skanuje nieprzetworzoną odpowiedź HTML w celu znalezienia i spekulacyjnego pobierania zasobów, zanim podstawowy parser HTML wykryje je w innym przypadku. Na przykład skaner wstępnego wczytywania umożliwi przeglądarce rozpoczęcie pobierania zasobu określonego w elemencie <img> nawet wtedy, gdy parser HTML jest zablokowany podczas pobierania i przetwarzania zasobów, takich jak CSS i JavaScript.

Aby można było korzystać ze skanera wstępnego wczytywania, ważne zasoby muszą być umieszczone w znacznikach HTML wysyłanych przez serwer. Skaner wstępnego wczytywania nie może wykryć tych wzorców wczytywania zasobów:

  • Obrazy wczytane przez CSS za pomocą właściwości background-image. Odwołania do obrazów są zapisane w CSS i nie mogą zostać wykryte przez skaner wstępnego wczytywania.
  • Wczytywane dynamicznie skrypty w postaci znaczników elementu <script> wstrzykiwanych do modelu DOM za pomocą JavaScriptu lub modułów wczytywanych za pomocą dynamicznego import().
  • Kod HTML wyrenderowany na kliencie za pomocą JavaScriptu. Znaczniki te są zawarte w ciągach tekstowych w zasobach JavaScript i nie są dostępne dla skanera wstępnego wczytywania.
  • Deklaracje CSS @import.

Te wzorce wczytywania zasobów to zasoby, które zostały wykryte późno i dlatego nie są one wykorzystywane przez skaner wstępnego wczytywania. Jeśli to możliwe, unikaj ich. Jeśli jednak unikanie takich wzorców nie jest możliwe, możesz skorzystać ze wskazówki preload, aby uniknąć opóźnień w odkrywaniu zasobów.

CSS

CSS określa wygląd i układ strony. Jak już wspomnieliśmy, CSS to zasób blokujący renderowanie, więc optymalizacja CSS może mieć znaczny wpływ na ogólny czas wczytywania strony.

Minifikacja

Zmniejszanie plików CSS zmniejsza rozmiar pliku zasobu CSS, dzięki czemu można je szybciej pobierać. Najczęściej usuwa się to ze źródłowego pliku CSS, np. spacje i inne niewidoczne znaki, i przesyła wynik do nowo zoptymalizowanego pliku:

/* Unminified CSS: */

/* Heading 1 */
h1 {
  font-size: 2em;
  color: #000000;
}

/* Heading 2 */
h2 {
  font-size: 1.5em;
  color: #000000;
}
/* Minified CSS: */
h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}

W najbardziej podstawowej formie minifikacja CSS to skuteczna optymalizacja, która może poprawić FCP witryny, a w niektórych przypadkach nawet LCP. Narzędzia takie jak pakiety mogą automatycznie przeprowadzać tę optymalizację w kompilacjach produkcyjnych.

Usuń nieużywany kod CSS

Przed wyrenderowaniem treści przeglądarka musi pobrać i przeanalizować wszystkie arkusze stylów. Czas potrzebny na ukończenie analizy obejmuje również style, które nie są używane na bieżącej stronie. Jeśli korzystasz z pakietu, który łączy wszystkie zasoby CSS w 1 pliku, użytkownicy prawdopodobnie pobierają więcej arkuszy CSS niż jest to konieczne do wyrenderowania bieżącej strony.

Aby wykryć nieużywany kod CSS na bieżącej stronie, użyj narzędzia Stan w indeksie w Narzędziach deweloperskich w Chrome.

Zrzut ekranu przedstawiający narzędzie Zasięg w Narzędziach deweloperskich w Chrome W dolnym panelu jest wybrany plik CSS, który pokazuje znaczną ilość kodu CSS nieużywanego w bieżącym układzie strony.
Narzędzie zasięgu w Narzędziach deweloperskich w Chrome przydaje się do wykrywania kodu CSS (i JavaScriptu) nieużywanych przez bieżącą stronę. Pozwala podzielić pliki CSS na wiele zasobów w celu ich wczytywania przez różne strony, zamiast wysyłać znacznie większy pakiet CSS, który może opóźniać renderowanie strony.

Usunięcie nieużywanego kodu CSS przynosi 2-etapowy skutek: oprócz skrócenia czasu pobierania optymalizujesz konstrukcję drzewa renderowania, ponieważ przeglądarka musi przetwarzać mniej reguł CSS.

Unikaj deklaracji CSS @import

Chociaż może się to wydawać dogodne, należy unikać deklaracji @import w CSS:

/* Don't do this: */
@import url('style.css');

Podobnie jak w przypadku elementu <link> w języku HTML, deklaracja @import w CSS pozwala importować zewnętrzny zasób CSS z arkusza stylów. Główna różnica między tymi 2 metodami polega na tym, że element HTML <link> jest częścią odpowiedzi HTML, więc jest wykrywany znacznie szybciej niż plik CSS pobrany przez deklarację @import.

Dzieje się tak, ponieważ aby deklaracja @import została wykryta, zawierający ją plik CSS trzeba najpierw pobrać. W efekcie powstaje tak zwany łańcuch żądań, który – w przypadku CSS – opóźnia pierwsze wyrenderowanie strony. Kolejną wadą jest to, że arkusze stylów wczytane za pomocą deklaracji @import nie są wykrywane przez skaner wstępnego wczytywania, co powoduje, że stają się one wykrytymi późno zasobami blokującymi renderowanie.

<!-- Do this instead: -->
<link rel="stylesheet" href="style.css">

W większości przypadków możesz zastąpić @import, używając elementu <link rel="stylesheet">. Elementy <link> umożliwiają pobieranie arkuszy stylów jednocześnie i skracają całkowity czas wczytywania w odróżnieniu od deklaracji @import, które pobierają arkusze stylów po kolei.

Wbudowany krytyczny kod CSS

Czas potrzebny na pobranie plików CSS może zwiększyć FCP strony. Wbudowanie stylów krytycznych w dokument <head> eliminuje żądanie sieciowe dotyczące zasobu CSS, a jeśli zostanie wykonane prawidłowo, może skrócić czas wczytywania, gdy pamięć podręczna przeglądarki użytkownika nie jest wstępnie przetworzona. Pozostały kod CSS można ładować asynchronicznie lub dołączać na końcu elementu <body>.

<head>
  <title>Page Title</title>
  <!-- ... -->
  <style>h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}</style>
</head>
<body>
  <!-- Other page markup... -->
  <link rel="stylesheet" href="non-critical.css">
</body>

Niestety wbudowanie dużej ilości kodu CSS powoduje dodanie większej liczby bajtów do początkowej odpowiedzi HTML. Zasoby HTML często nie mogą być przechowywane w pamięci podręcznej przez bardzo długi czas – lub w ogóle nie można ich przechowywać w pamięci podręcznej. Oznacza to, że wbudowany arkusz CSS nie jest przechowywany w pamięci podręcznej kolejnych stron, które mogą używać tego samego kodu CSS w zewnętrznych arkuszach stylów. Przetestuj i mierz wydajność strony, aby się przekonać, czy warto poświęcać czas na te korzyści.

Wersje demonstracyjne CSS

JavaScript

JavaScript odpowiada za większość interakcji w internecie, ale wiąże się to z kosztami. Przesłanie zbyt dużej ilości kodu JavaScript może spowolnić jego reagowanie podczas wczytywania strony, a nawet spowodować problemy z responsywnością, które spowalniają interakcje – oba te sytuacje mogą być frustrujące dla użytkowników.

JavaScript blokujący renderowanie

Podczas wczytywania elementów <script> bez atrybutów defer i async przeglądarka blokuje analizowanie i renderowanie do czasu pobrania, analizy i wykonania skryptu. Podobnie skrypty wbudowane blokują parser do czasu analizy i wykonania skryptu.

async kontra defer

async i defer umożliwiają ładowanie skryptów zewnętrznych bez blokowania parsera HTML, a skrypty (w tym skrypty wbudowane) z interfejsem type="module" są odroczone automatycznie. async i defer mają jednak pewne różnice, o których trzeba pamiętać.

Ilustracja przedstawiająca różne mechanizmy ładowania skryptów, zawierające szczegółowe informacje o rolach parsera, pobierania i wykonywania w oparciu o różne używane atrybuty, takie jak asynchroniczny, opóźniony, type=&#39;module&#39; oraz połączenie wszystkich 3 elementów.
Źródło: https://html.spec.whatwg.org/multipage/scripting.html

Skrypty załadowane za pomocą async są analizowane i wykonywane natychmiast po pobraniu, a skrypty ładowane za pomocą defer są wykonywane po zakończeniu analizy dokumentu HTML (następuje to w tym samym czasie co zdarzenie DOMContentLoaded przeglądarki). Dodatkowo skrypty async mogą wykonywać się w niewłaściwej kolejności, a skrypty defer są wykonywane w takiej kolejności, w jakiej występują w znacznikach.

Renderowanie po stronie klienta

Ogólnie nie należy używać JavaScriptu do renderowania żadnych treści o znaczeniu krytycznym ani elementu LCP strony. Ta technika nosi nazwę renderowania po stronie klienta i jest często stosowana w aplikacjach jednostronicowych (SPA).

Znaczniki renderowane przez JavaScript pomijają skaner wstępnego wczytywania, ponieważ zasoby zawarte w znacznikach renderowanych przez klienta nie są przez niego wykrywane. Może to opóźnić pobieranie kluczowych zasobów, takich jak obraz LCP. Przeglądarka rozpoczyna pobieranie obrazu LCP dopiero po wykonaniu skryptu i dodanie elementu do modelu DOM. Z kolei skrypt może być uruchomiony dopiero po jego wykryciu, pobraniu i przeanalizowaniu. Jest to tzw. krytyczny łańcuch żądań, którego należy unikać.

Poza tym renderowanie znaczników za pomocą JavaScriptu z większym prawdopodobieństwem wygeneruje długie zadania niż znaczniki pobrane z serwera w odpowiedzi na żądanie nawigacji. Intensywne korzystanie z renderowania kodu HTML po stronie klienta może negatywnie wpływać na czas oczekiwania na interakcję. Dotyczy to zwłaszcza sytuacji, gdy DOM strony jest bardzo duży, co wymaga znacznych nakładów pracy podczas renderowania, gdy JavaScript modyfikuje DOM.

Minifikacja

Podobnie jak w przypadku CSS, zmniejszanie kodu JavaScript zmniejsza rozmiar pliku zasobu skryptu. Może to prowadzić do szybszego pobierania, dzięki czemu przeglądarka może szybciej zająć się analizą i kompilacją kodu JavaScript.

Ponadto zmniejszanie kodu JavaScript idzie o krok dalej niż zmniejszanie innych zasobów, takich jak CSS. Gdy kod JavaScript jest zmniejszony, nie tylko jest usuwany z takich elementów jak spacje, karty czy komentarze, ale także skraca symbole w źródłowym kodzie JavaScript. Ten proces jest czasami określany jako uglify. Aby zobaczyć różnicę, skorzystaj z tego kodu źródłowego JavaScript:

// Unuglified JavaScript source code:
export function injectScript () {
  const scriptElement = document.createElement('script');
  scriptElement.src = '/js/scripts.js';
  scriptElement.type = 'module';

  document.body.appendChild(scriptElement);
}

Jeśli poprzedni kod źródłowy JavaScript jest usztywniony, wynik może wyglądać mniej więcej tak:

// Uglified JavaScript production code:
export function injectScript(){const t=document.createElement("script");t.src="/js/scripts.js",t.type="module",document.body.appendChild(t)}

We fragmencie kodu widać, że zrozumiała dla człowieka zmienna scriptElement w źródle została skrócona do t. W przypadku dużej kolekcji skryptów oszczędność ta może być znaczna, ale nie wpływa na funkcje dostępne w przypadku kodu JavaScriptu do obsługi produkcyjnej witryny.

Jeśli do przetwarzania kodu źródłowego witryny używasz narzędzia do tworzenia pakietów, w przypadku kompilacji produkcyjnych uwierzytelnianie często jest wykonywane automatycznie. Ulotki, takie jak Terser, również można bardzo łatwo konfigurować, co pozwala dostosować jego agresywność w celu osiągnięcia maksymalnych oszczędności. Jednak aby zachować odpowiednią równowagę między rozmiarem danych wyjściowych a zachowaniem możliwości, w przypadku każdego narzędzia do usprawniania zwykle wystarcza domyślne ustawienia.

Wersje demonstracyjne JavaScript

Sprawdź swoją wiedzę

Jaki jest najlepszy sposób na wczytanie wielu plików CSS w przeglądarce?

Deklaracja CSS @import.
Spróbuj ponownie.
Wiele elementów <link>.
Dobrze!

Co robi skaner wstępnego wczytywania przeglądarki?

Jest to dodatkowy parser HTML, który bada nieprzetworzone znaczniki w celu wykrycia zasobów, zanim parser DOM może je wykryć.
Dobrze!
Wykrywa elementy <link rel="preload"> w zasobie HTML.
Spróbuj ponownie.

Dlaczego przeglądarka domyślnie blokuje analizowanie kodu HTML podczas pobierania zasobów JavaScript?

Aby zapobiec wyświetlaniu Flash of Unstyled Content (FOUC).
Spróbuj ponownie.
Ponieważ ocenianie kodu JavaScript jest zadaniem bardzo obciążającym procesor, a wstrzymanie analizy HTML zwiększa przepustowość procesora w celu zakończenia wczytywania skryptów.
Spróbuj ponownie.
Ponieważ skrypty mogą modyfikować DOM lub uzyskiwać do niego dostęp w inny sposób.
Dobrze!

Kolejny krok: wspomaganie przeglądarki przy użyciu wskazówek dotyczących zasobów

Wiesz już, jak zasoby ładowane w elemencie <head> mogą wpływać na początkowe wczytanie strony i różne wskaźniki. Pora przejść dalej. W następnym module omówimy wskazówki dotyczące zasobów i sposoby, w jakie mogą one przekazywać przeglądarce cenne wskazówki, jak rozpocząć ładowanie zasobów i otwierać połączenia z serwerami z innych domen wcześniej niż przeglądarka bez nich.