Analizowanie wydajności krytycznej ścieżki renderowania

Identyfikowanie i rozwiązywanie problemów związanych z wąskimi gardłami wydajności ścieżek renderowania wymaga dobrej znajomości typowych błędów. Przyjrzyjmy się, jak wyodrębniać typowe wzorce wydajności, które pomogą Ci zoptymalizować strony.

Optymalizacja krytycznej ścieżki renderowania umożliwia przeglądarce szybsze wyświetlanie strony – to przekłada się na większe zaangażowanie, większą liczbę wyświetlonych stron i większą liczbę konwersji. Aby zminimalizować czas, przez jaki użytkownik widzi pusty ekran, musimy zoptymalizować zasoby i ich kolejność.

Aby przedstawić ten proces, zaczniemy od najprostszego przypadku i stopniowo rozbudowujmy naszą stronę, dodając do niej dodatkowe zasoby, style i logikę aplikacji. W trakcie tego procesu optymalizujemy każdy przypadek, widzimy też, co może pójść nie tak.

Do tej pory skupialiśmy się wyłącznie na tym, co dzieje się w przeglądarce po udostępnieniu zasobu (CSS, JS lub HTML) do przetworzenia. Nie uwzględniamy czasu potrzebnego na pobranie zasobu z pamięci podręcznej lub z sieci. Przyjęliśmy takie założenia:

  • Przesyłanie danych w obie strony (opóźnienie propagacji) do serwera kosztuje 100 ms.
  • Czas reakcji serwera wynosi 100 ms w przypadku dokumentu HTML i 10 ms w przypadku wszystkich innych plików.

Pakiet „hello world”

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critical Path: No Style</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

Wypróbuj

Zaczniemy od podstawowych znaczników HTML i jednego obrazu, bez arkuszy CSS czy JavaScript. Otwieramy oś czasu naszej sieci w Narzędziach deweloperskich w Chrome i przyjrzymy się wynikowej kaskadzie zasobów:

CRP

Zgodnie z oczekiwaniami pobieranie pliku HTML trwało około 200 ms. Przezroczysta część niebieskiej linii oznacza czas, przez jaki przeglądarka oczekuje na połączenie w sieci bez odbierania żadnych bajtów odpowiedzi, natomiast ciągła część wskazuje czas do zakończenia pobierania po otrzymaniu pierwszych bajtów odpowiedzi. Plik HTML do pobrania jest niewielki (poniżej 4K), więc wystarczy jedno przesyłanie w obie strony. W związku z tym pobieranie dokumentu HTML trwa około 200 ms, przy czym połowa czasu oczekiwania na połączenie z siecią, a druga połowa – na odpowiedź serwera.

Po udostępnieniu treści HTML przeglądarka analizuje bajty, konwertuje je na tokeny i buduje drzewo DOM. Zwróć uwagę, że na dole (216 ms) DevTools w wygodny sposób raportuje czas zdarzenia DOMContentLoaded, który również odpowiada niebieskiej pionowej linii. Przerwa między końcem pobieranego pliku HTML a niebieską linią pionową (DOMContentLoaded) oznacza czas potrzebny przeglądarce na zbudowanie drzewa DOM – w tym przypadku zaledwie kilka milisekund.

Zwróć uwagę, że nasze „świetne zdjęcie” nie zablokuje wydarzenia domContentLoaded. Okazuje się, że możemy zbudować drzewo renderowania, a nawet pomalować stronę, nie czekając na każdy zasób na stronie – nie wszystkie zasoby są niezbędne do szybkiego pierwszego wyrenderowania. Gdy mówimy o krytycznej ścieżce renderowania, zazwyczaj mamy na myśli znaczniki HTML oraz CSS i JavaScript. Obrazy nie blokują początkowego renderowania strony, ale warto też jak najszybciej zacząć korzystać z tych obrazów.

Pamiętaj jednak, że zdarzenie load (nazywane też onload) jest zablokowane na obrazie. Narzędzia deweloperskie zgłaszają zdarzenie onload z czasem 335 ms. Pamiętaj, że zdarzenie onload wskazuje miejsce, w którym wszystkie zasoby, których wymaga dana strona, zostały pobrane i przetworzone. W tym momencie ikona wczytywania może przestać się obracać w przeglądarce (czerwona pionowa linia w kaskadzie).

Dodanie do zestawu narzędzi JavaScript i CSS

Strona „Hello World” wydaje się prosta, ale zawiera sporo rzeczy. W praktyce potrzebny jest nie tylko kod HTML – możliwe, że dysponujemy arkuszem stylów CSS i co najmniej jednym skryptem, które dodają elementy interaktywne do strony. Dodajmy oba te elementy do listy i sprawdźmy, co się stanie:

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Script</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body onload="measureCRP()">
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="timing.js"></script>
  </body>
</html>

Wypróbuj

Przed dodaniem JavaScriptu i arkusza CSS:

DOM CRP

JavaScript i CSS:

DOM, CSSOM, JS

Dodanie zewnętrznych plików CSS i JavaScript powoduje dodanie do naszej kaskady 2 dodatkowych żądań, które przeglądarka wysyła mniej więcej w tym samym czasie. Zwróć jednak uwagę, że teraz występuje znacznie mniejsza różnica czasu między zdarzeniami domContentLoaded i onload.

What happened?

  • W przeciwieństwie do przykładu w postaci zwykłego kodu HTML, aby utworzyć drzewo renderowania, musimy pobrać i przeanalizować plik CSS, a do utworzenia drzewa renderowania potrzebujemy zarówno DOM, jak i CSSOM.
  • Strona zawiera też plik JavaScript blokujący parser, więc zdarzenie domContentLoaded jest blokowane do czasu pobrania i przeanalizowania pliku CSS. Kod JavaScript może wysyłać zapytania do CSSOM, dlatego musimy zablokować plik CSS do czasu jego pobrania, zanim będzie można wykonać kod JavaScript.

Co się stanie, jeśli zastąpimy skrypt zewnętrzny skryptem wbudowanym? Nawet jeśli skrypt jest wbudowany bezpośrednio w stronę, przeglądarka nie może go wykonać, dopóki nie utworzysz arkusza CSSOM. W skrócie: kod JavaScript w tekście blokuje też działanie parsera.

Czy pomimo zablokowania w CSS, wbudowanie skryptu sprawia, że strona renderuje się szybciej? Wypróbujmy to i zobaczmy, co się stanie.

Zewnętrzny JavaScript:

DOM, CSSOM, JS

JavaScript w tekście:

DOM, CSSOM i wbudowany JS

Wysyłamy o jedne żądanie mniej, ale wartości czasu onload i domContentLoaded są w praktyce takie same. Dlaczego? Wiemy, że nie ma znaczenia, czy kod JavaScript jest umieszczony w tekście czy na zewnątrz, ponieważ z chwilą uruchomienia tagu skryptu przeglądarka blokuje go i czeka na utworzenie CSSOM. Ponadto w pierwszym przykładzie przeglądarka pobiera równolegle pliki CSS i JavaScript i kończy je mniej więcej w tym samym czasie. W tym przypadku niewiele pomaga nam wbudowanie kodu JavaScript. Istnieje jednak kilka strategii, które mogą przyspieszyć renderowanie naszej strony.

Po pierwsze, pamiętaj, że wszystkie skrypty wbudowane blokują parser, ale w przypadku skryptów zewnętrznych możemy dodać słowo kluczowe „asynchroniczne”, aby odblokować parser. Cofnijmy wprowadzenie i spróbujmy:

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Async</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body onload="measureCRP()">
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script async src="timing.js"></script>
  </body>
</html>

Wypróbuj

JavaScript blokujący (zewnętrzny):

DOM, CSSOM, JS

Asynchroniczny (zewnętrzny) JavaScript:

DOM, CSSOM, asynchroniczny JS

Dużo lepiej! Zdarzenie domContentLoaded uruchamia się wkrótce po przeanalizowaniu kodu HTML. Przeglądarka wie, że nie należy blokować kodu JavaScript, a ponieważ nie ma innych skryptów blokujących parser, konstrukcja CSSOM również może być kontynuowana równolegle.

Możemy też wstawić zarówno kod CSS, jak i JavaScript:

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Inlined</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <style>
      p {
        font-weight: bold;
      }
      span {
        color: red;
      }
      p span {
        display: none;
      }
      img {
        float: right;
      }
    </style>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script>
      var span = document.getElementsByTagName('span')[0];
      span.textContent = 'interactive'; // change DOM text content
      span.style.display = 'inline'; // change CSSOM property
      // create a new element, style it, and append it to the DOM
      var loadTime = document.createElement('div');
      loadTime.textContent = 'You loaded this page on: ' + new Date();
      loadTime.style.color = 'blue';
      document.body.appendChild(loadTime);
    </script>
  </body>
</html>

Wypróbuj

DOM, CSS w treści, JS w tekście

Zwróć uwagę, że czas domContentLoaded jest w praktyce taki sam jak w poprzednim przykładzie. Zamiast oznaczać kod JavaScript jako asynchroniczny, wbudowaliśmy w stronę kod CSS i JS. Dzięki temu strona HTML jest znacznie większa, ale dodatkową zaletą jest to, że przeglądarka nie musi czekać na pobranie zasobów zewnętrznych – wszystko znajduje się na stronie.

Jak widać, nawet przy bardzo prostej stronie optymalizacja krytycznej ścieżki renderowania nie jest trywialna: musimy zrozumieć wykres zależności między różnymi zasobami, określić, które zasoby są „krytyczne”, i wybrać spośród różnych strategii uwzględnianie tych zasobów na stronie. Nie ma jednego rozwiązania tego problemu, każda strona jest inna. Aby wybrać optymalną strategię, musisz samodzielnie przeprowadzić podobny proces.

Zobaczmy, czy uda nam się cofnąć i zidentyfikować jakieś ogólne wzorce skuteczności.

Wzorce skuteczności

Najprostsza strona składa się tylko ze znaczników HTML, bez arkuszy CSS, kodu JavaScript ani innych rodzajów zasobów. Aby wyrenderować tę stronę, przeglądarka musi zainicjować żądanie, poczekać na pojawienie się dokumentu HTML, przeanalizować go, utworzyć model DOM, a na koniec wyrenderować go na ekranie:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critical Path: No Style</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

Wypróbuj

CRP (Witaj świecie)

Czas między T0 a T1 rejestruje czas przetwarzania sieci i serwera. W najlepszym przypadku (jeśli plik HTML jest mały) tylko jedna sekwencja sieciowa spowoduje pobranie całego dokumentu. Ze względu na sposób działania protokołów transportu TCP większe pliki mogą wymagać przesyłania w obie strony. W rezultacie powyższa strona ma co najmniej 1 krytyczną ścieżkę renderowania w obie strony.

A teraz zajmijmy się tą samą stroną, ale z zewnętrznym plikiem CSS:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

Wypróbuj

DOM + CSSOM CRP

Ponownie przesyłamy błąd sieciowy w celu pobrania dokumentu HTML, po czym pobrane znaczniki informują nas, że potrzebujemy także pliku CSS. Oznacza to, że aby wyrenderować stronę na ekranie, przeglądarka musi wrócić do serwera i pobrać kod CSS. W efekcie strona musi zostać wyświetlona co najmniej 2 razy w obie strony. Tak jak wspomniano wcześniej, plik CSS może zajmować wiele transferów w obie strony, stąd nacisk na „minimum”.

Zdefiniujmy słownictwo, którego używamy do opisania krytycznej ścieżki renderowania:

  • Zasób krytyczny: zasób, który może zablokować wstępne renderowanie strony.
  • Długość ścieżki krytycznej: liczba cykli wymiany danych lub łączny czas potrzebny do pobrania wszystkich zasobów krytycznych.
  • Bajty krytyczne: łączna liczba bajtów wymaganych do pierwszego wyrenderowania strony, będąca sumą rozmiarów plików transferu wszystkich zasobów krytycznych. Pierwszy przykład z pojedynczą stroną HTML zawierał jeden zasób krytyczny (dokument HTML). Długość ścieżki krytycznej była również równa jednej cyklowi przesyłania danych w obie strony (przy założeniu, że plik był mały), a łączna liczba bajtów krytycznych to rozmiar transferu samego dokumentu HTML.

Porównajmy te cechy z charakterystycznymi cechami ścieżki krytycznej w przykładzie powyżej dotyczącym kodu HTML i CSS:

DOM + CSSOM CRP

  • 2 kluczowe zasoby
  • 2 lub więcej cykli wymiany danych w obrębie minimalnej długości ścieżki krytycznej
  • 9 KB bajtów krytycznych

Do utworzenia drzewa renderowania potrzebny jest zarówno kod HTML, jak i CSS. Kod HTML i CSS mają kluczowe znaczenie – kod CSS jest pobierany dopiero po tym, jak przeglądarka pobierze dokument HTML, a dlatego długość ścieżki krytycznej nie powinna przekraczać dwóch cykli wymiany danych. Oba zasoby sumują się do 9 KB krytycznych bajtów.

Teraz dodajmy do zestawu dodatkowy plik JavaScript.

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js"></script>
  </body>
</html>

Wypróbuj

Dodaliśmy na stronie app.js, który jest zarówno zewnętrznym zasobem JavaScriptu na stronie, jak i zasobem blokującym (krytyczny) zasób analizujący. Co gorsza, aby wykonać plik JavaScript, musimy go zablokować i oczekiwać na kod CSSOM. Pamiętaj, że JavaScript może wysyłać zapytania do CSSOM, więc przeglądarka zostaje wstrzymana do czasu pobrania style.css i utworzenia CSSOM.

DOM, CSSOM, JavaScript CRP

Jednak w praktyce przyjrzymy się „kaskadzie sieci” tej strony, a żądania CSS i JavaScript są inicjowane mniej więcej w tym samym czasie – przeglądarka pobiera kod HTML, wykrywa oba zasoby i inicjuje oba żądania. W związku z tym powyższa strona ma te cechy ścieżki krytycznej:

  • 3 kluczowe zasoby
  • 2 lub więcej cykli wymiany danych w obrębie minimalnej długości ścieżki krytycznej
  • 11 KB bajtów krytycznych

Mamy obecnie 3 zasoby krytyczne, które składają się na 11 KB bajtów krytycznych, ale ścieżka krytycznej ścieżki nadal wynosi 2 cykle wymiany danych, ponieważ możemy przesyłać równolegle pliki CSS i JavaScript. Określenie charakterystyki krytycznej ścieżki renderowania oznacza, że trzeba być w stanie zidentyfikować zasoby kluczowe oraz zrozumieć, jak przeglądarka planuje ich pobieranie. Przeanalizujmy nasz przykład.

Po rozmowie z programistami witryn stwierdzamy, że kod JavaScript umieszczony na stronie nie musi być blokowany. Zawiera on funkcje analityczne i inne fragmenty kodu, które nie muszą blokować renderowania strony. Wiedząc to, możemy dodać do tagu skryptu atrybut „async”, by odblokować parser:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js" async></script>
  </body>
</html>

Wypróbuj

DOM, CSSOM, asynchroniczny JavaScript CRP

Skrypt asynchroniczny ma kilka zalet:

  • Skrypt nie blokuje już parsera i nie należy do krytycznej ścieżki renderowania.
  • Nie ma innych skryptów o znaczeniu krytycznym, dlatego CSS nie musi blokować zdarzenia domContentLoaded.
  • Im szybciej zostanie uruchomione zdarzenie domContentLoaded, tym szybciej rozpocznie się wykonywanie innej logiki aplikacji.

W rezultacie nasza zoptymalizowana strona powraca do korzystania z 2 zasobów krytycznych (HTML i CSS) o minimalnej długości ścieżki krytycznej wynoszącej 2 cykle wymiany danych i łącznie 9 KB bajtów krytycznych.

Na koniec – jak wyglądałaby sytuacja, gdyby arkusz stylów CSS był potrzebny tylko do wydrukowania?

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" media="print" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js" async></script>
  </body>
</html>

Wypróbuj

DOM, nieblokujący CSS i asynchroniczny kod JavaScript CRP

Zasób style.css jest używany tylko do drukowania, więc przeglądarka nie musi go blokować, by wyświetlić stronę. Dlatego gdy tylko zakończy się konstrukcja DOM, przeglądarka ma wystarczającą ilość informacji, aby wyświetlić stronę. W efekcie ta strona ma tylko jeden krytyczny zasób (dokument HTML), a minimalna krytyczna ścieżka renderowania to jedna porcja w obie strony.

Prześlij opinię