Data publikacji: 31 marca 2014 r.
Aby wykrywać i rozwiązywać problemy z wydajnością na ścieżce renderowania, trzeba dobrze znać typowe pułapki. Wprowadzenie, które pomoże Ci zidentyfikować typowe wzorce skuteczności, ułatwi Ci optymalizację stron.
Optymalizacja ścieżki renderowania krytycznego pozwala przeglądarce wyświetlać stronę tak szybko, jak to możliwe. Szybsze wczytywanie stron zwiększa zaangażowanie użytkowników, liczbę wyświetlanych stron i poprawia skuteczność konwersji. Aby zminimalizować czas, jaki użytkownik spędza na oglądaniu pustego ekranu, musimy zoptymalizować kolejność wczytywania zasobów.
Aby zilustrować ten proces, zacznij od najprostszego możliwego przypadku i stopniowo rozbudowuj stronę, aby zawierała dodatkowe zasoby, style i logikę aplikacji. W tym celu zoptymalizujemy każdy przypadek i sprawdzimy, gdzie mogą wystąpić problemy.
Do tej pory skupialiśmy się wyłącznie na tym, co dzieje się w przeglądarce po tym, jak zasób (plik CSS, JS lub HTML) jest dostępny do przetwarzania. Ignorujemy czas potrzebny na pobranie zasobu z pamięci podręcznej lub z sieci. Zakładamy, że:
- Czas błądzenia w sieci (opóźnienie propagacji) do serwera wynosi 100 ms.
- Czas odpowiedzi serwera wynosi 100 ms w przypadku dokumentu HTML i 10 ms w przypadku wszystkich innych plików.
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>
Zacznij od podstawowego oznaczenia HTML i jednego obrazu bez użycia kodu CSS ani JavaScript. Następnie otwórz panel Sieć w Narzędziach deweloperskich w Chrome i sprawdź uzyskaną kaskadę zasobów:
Zgodnie z oczekiwaniami pobranie pliku HTML zajęło około 200 ms. Zwróć uwagę, że przezroczysta część niebieskiej linii oznacza czas, przez jaki przeglądarka oczekuje na połączenie z siecią bez odbierania bajtów odpowiedzi, a ciągła część to czas na zakończenie pobierania po otrzymaniu pierwszych bajtów odpowiedzi. Plik HTML do pobrania jest niewielki (mniej niż 4 KB), więc do pobrania całego pliku wystarczy nam 1 przesył. W rezultacie pobieranie dokumentu HTML zajmuje około 200 ms, z czym połowa tego czasu to oczekiwanie na odpowiedź sieci, a druga połowa – na odpowiedź serwera.
Po udostępnieniu treści HTML przeglądarka analizuje bajty, konwertuje je na tokeny i tworzy drzewo DOM. Zauważ, że DevTools wygodnie podaje czas zdarzenia DOMContentLoaded na dole (216 ms), który odpowiada również niebieskiej linii pionowej. Przerwa między końcem pobierania HTML a niebieską linią pionową (DOMContentLoaded) to czas potrzebny przeglądarce na utworzenie drzewa DOM. W tym przypadku to tylko kilka milisekund.
Zwróć uwagę, że nasze „świetne zdjęcie” nie zablokowało zdarzenia domContentLoaded
. Okazuje się, że możemy zbudować drzewo renderowania i nawet wyrenderować stronę bez oczekiwania na każdy komponent na stronie: nie wszystkie zasoby są niezbędne do szybkiego wyrenderowania strony. Gdy mówimy o krytycznej ścieżce renderowania, zwykle mamy na myśli znaczniki HTML, CSS i JavaScript. Obrazy nie blokują początkowego renderowania strony, ale powinniśmy też jak najszybciej wyświetlić obrazy.
Mimo to zdarzenie load
(nazywane też onload
) jest zablokowane na obrazie: Narzędzia deweloperskie zgłaszają zdarzenie onload
przy 335 ms. Pamiętaj, że zdarzenie onload
oznacza moment, w którym wszystkie zasoby wymagane przez stronę zostały pobrane i przetworzone. W tym momencie wskaźnik ładowania może przestać się obracać w przeglądarce (czerwona pionowa linia w schemacie kaskadowym).
Dodaj JavaScript i CSS
Strona „Hello World” może wydawać się prosta, ale pod maską dzieje się dużo. W praktyce potrzebujemy czegoś więcej niż tylko kod HTML. Możliwe, że będziemy mieli arkusz stylów CSS i co najmniej jeden skrypt, aby wzbogacić naszą stronę o interaktywność. Aby przekonać się, jaki efekt uzyskasz, dodaj do niego oba te elementy:
<!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>
Przed dodaniem kodu JavaScript i CSS:
Za pomocą JavaScript i CSS:
Dodanie zewnętrznych plików CSS i JavaScriptu powoduje dodanie do naszej kaskady 2 dodatkowych żądań, które przeglądarka wysyła mniej więcej w tym samym czasie. Należy jednak pamiętać, że różnica między zdarzeniami domContentLoaded
i onload
jest teraz znacznie mniejsza.
Co się stało?
- W przeciwieństwie do zwykłego przykładu w języku HTML musimy też pobrać i przeanalizować plik CSS, aby utworzyć CSSOM, a do zbudowania drzewa renderowania potrzebujemy zarówno DOM, jak i CSSOM.
- Strona zawiera też plik JavaScript, który blokuje parsowanie, więc zdarzenie
domContentLoaded
jest blokowane, dopóki plik CSS nie zostanie pobrany i przetworzony. Ponieważ kod JavaScript może wysyłać zapytania do CSSOM, musimy zablokować plik CSS, dopóki nie zostanie pobrany, zanim będziemy mogli wykonać kod JavaScript.
Co się stanie, jeśli zastąpimy zewnętrzny skrypt skryptem wbudowanym? Nawet jeśli skrypt jest wbudowany bezpośrednio w stronę, przeglądarka nie może go wykonać, dopóki nie zostanie utworzony CSSOM. Krótko mówiąc, wbudowany JavaScript też blokuje parsowanie.
Czy mimo zablokowania w kodzie CSS strona wbudowana w skrypt przyspiesza renderowanie strony? Wypróbuj i zobacz, co się stanie.
Zewnętrzny kod JavaScript:
Wstawiony kod JavaScript:
Wysyłamy o 1 żądanie mniej, ale czasy onload
i domContentLoaded
są w zasadzie takie same. Dlaczego? Wiemy, że nie ma znaczenia, czy JavaScript jest wbudowany czy zewnętrzny, ponieważ gdy tylko przeglądarka trafi w tag skryptu, blokuje się i czeka na utworzenie CSSOM. W pierwszym przykładzie przeglądarka pobiera jednocześnie pliki CSS i JavaScript, a ich pobieranie kończy się mniej więcej w tym samym czasie. W tym przypadku wstawienie kodu JavaScript w tekście nie przynosi większych korzyści. Istnieje jednak kilka strategii, które mogą przyspieszyć renderowanie strony.
Najpierw przypomnę, że wszystkie skrypty wstawiane inline blokują parsowanie, ale w przypadku skryptów zewnętrznych możemy dodać atrybut async
, aby odblokować parsowanie. Odwróć wstawienie kodu i spróbuj:
<!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>
Blokowanie parsowania (zewnętrzny) JavaScript:
Asynchroniczny (zewnętrzny) kod JavaScript:
O wiele lepiej. Zdarzenie domContentLoaded
jest wywoływane tuż po przeanalizowaniu kodu HTML. Przeglądarka wie, że nie ma blokować kodu JavaScript, a ponieważ nie ma też żadnych innych blokujących skryptów, budowa obiektu CSSOM może być wykonywana równolegle.
Moglibyśmy też umieścić w tekście zarówno 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>
Zwróć uwagę, że czas domContentLoaded
jest taki sam jak w poprzednim przykładzie. Zamiast oznaczać kod JavaScript jako asynchroniczny, wbudowaliśmy zarówno kod CSS, jak i JS w samej stronie. Spowoduje to, że nasza strona HTML będzie znacznie większa, ale plusem jest to, że przeglądarka nie musi czekać na pobranie żadnych zewnętrznych zasobów; wszystko znajduje się na stronie.
Jak widzisz, nawet w przypadku bardzo prostej strony optymalizacja ścieżki renderowania krytycznego nie jest prostym zadaniem: musisz zrozumieć zależność między różnymi zasobami, określić, które z nich są „krytyczne”, a potem wybrać jedną z różnych strategii ich włączania na stronie. Nie ma jednego rozwiązania tego problemu – każda strona jest inna. Aby znaleźć optymalną strategię, musisz samodzielnie wykonać podobny proces.
Zobaczmy, czy uda nam się zidentyfikować ogólne wzorce skuteczności.
Wzorce wydajności
Najprostsza możliwa strona składa się tylko ze znaczników HTML, bez kodu CSS, JavaScript ani innych typów zasobów. Aby ją wyrenderować, przeglądarka musi zainicjować żądanie, zaczekać na przybycie dokumentu HTML, przeanalizować go, utworzyć element 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>
Czas między T0 a T1 określa czas przetwarzania sieci i serwera. W najlepszym przypadku (jeżeli plik HTML jest mały), cały dokument zostanie pobrany w ramach jednej operacji przesyłania danych w obie strony. Ze względu na sposób działania protokołów transportowych TCP większe pliki mogą wymagać więcej przesyłań w obie strony. W efekcie w najlepszym przypadku strona ta ma ścieżkę renderowania w obie strony (minimalną) na potrzeby renderowania.
Teraz weź pod uwagę 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>
Razem ponownie, pobieramy dokument HTML w obie strony, po czym za pomocą pobranych znaczników wiemy, że potrzebujemy pliku CSS. To oznacza, że przeglądarka musi wrócić do serwera i pobrać kod CSS, aby wyświetlić stronę na ekranie. W wyniku tego wyświetlenie tej strony wymaga co najmniej 2 przesłań w obie strony. Plik CSS może wymagać kilku przesyłań w obie strony, dlatego kładziemy nacisk na „minimum”.
Oto kilka terminów, których używamy do opisania krytycznej ścieżki renderowania:
- Zasób krytyczny: zasób, który może zablokować początkowe renderowanie strony.
- Długość ścieżki krytycznej: liczba przejazdów w obie strony lub łączny czas potrzebny do pobrania wszystkich krytycznych zasobów.
- Kluczowe bajty: łączna liczba bajtów wymagana do pierwszego renderowania strony, czyli suma rozmiarów plików do przesyłania wszystkich kluczowych zasobów. Nasz pierwszy przykład, czyli pojedyncza strona HTML, zawierał 1 krytyczny zasób (dokument HTML). Długość ścieżki krytycznej była równa 1 przesyłce w obie strony (zakładając, że plik był mały), a łączna liczba bajtów krytycznych to tylko rozmiar przesyłanego dokumentu HTML.
Teraz porównajmy to z cechami krytymi ścieżki z poprzedniego przykładu kodu HTML i CSS:
- 2 najważniejsze zasoby
- 2 lub więcej przejazdów w obie strony na minimalnej długości ścieżki krytycznej
- 9 KB danych krytycznych
Do tworzenia drzewa renderowania potrzebujemy zarówno kodu HTML, jak i CSS. W efekcie zarówno HTML, jak i CSS są kluczowymi zasobami: kod CSS jest pobierany dopiero po tym, jak przeglądarka pobierze dokument HTML, a ścieżka krytyczna musi zawierać co najmniej 2 przebiegi w obie strony. Oba zasoby łącznie zajmują 9 KB.
Teraz dodaj 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>
Dodaliśmy app.js
, który jest zewnętrznym komponentem JavaScriptu na stronie i zasobom blokującym parsowanie (czyli krytycznym). Co gorsza, aby wykonać plik JavaScript, musimy zablokować i odczekać na obiekt CSSOM. Pamiętaj, że JavaScript może wysyłać zapytania do obiektu CSSOM, a przeglądarka wstrzymuje się, dopóki plik style.css
nie zostanie pobrany i nie zostanie utworzony obiekt CSSOM.
W praktyce jednak, jeśli spojrzymy na „schemat sieci” tej strony, zobaczymy, że żądania CSS i JavaScript są inicjowane mniej więcej w tym samym czasie. Przeglądarka pobiera kod HTML, odkrywa oba zasoby i inicjuje żądania. W związku z tym strona widoczna na poprzednim obrazie ma te cechy ścieżki krytycznej:
- 3 najważniejsze zasoby
- 2 lub więcej przejazdów w obie strony na minimalnej długości ścieżki krytycznej
- 11 KB danych krytycznych
Mamy teraz 3 krytyczne zasoby, które łącznie zajmują 11 KB, ale długość krytycznej ścieżki to nadal 2 przesyłanie w obie strony, ponieważ możemy przesyłać kod CSS i JavaScript równolegle. Określenie charakterystyki krytycznej ścieżki renderowania wymaga możliwości identyfikacji kluczowych zasobów oraz określenia, w jaki sposób przeglądarka planuje ich pobieranie.
Po rozmowie z programistami naszej witryny zdaliśmy sobie sprawę, że kod JavaScript, który umieściliśmy na stronie, nie musi być blokowany. Mamy tam kod analityczny i inne elementy, które nie muszą blokować renderowania strony. Dzięki temu możemy dodać atrybut async
do elementu <script>
, aby odblokować parsowanie:
<!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>
Skrypt asynchroniczny ma kilka zalet:
- Skrypt nie blokuje już parsera i nie należy do krytycznej ścieżki renderowania.
- Ponieważ nie ma innych kluczowych skryptów, skrypt CSS nie musi blokować zdarzenia
domContentLoaded
. - Im szybciej uruchomi się zdarzenie
domContentLoaded
, tym szybciej rozpocznie się wykonywanie innych logiki aplikacji.
W rezultacie nasza zoptymalizowana strona wraca do 2 zasobów kluczowych (HTML i CSS) o minimalnej długości ścieżki krytycznej wynoszącej 2 przebiegi w obie strony i łącznie 9 kB krytycznych bajtów.
Na koniec, jak wyglądałoby to, gdyby arkusz stylów CSS był potrzebny tylko do wersji drukowanej?
<!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>
Ponieważ zasób style.css jest używany tylko do drukowania, przeglądarka nie musi go blokować, aby renderować stronę. Dlatego, gdy tylko zakończy się tworzenie modelu DOM, przeglądarka ma wystarczającą ilość informacji do wyrenderowania strony. W efekcie na tej stronie znajduje się tylko 1 zasób krytyczny (dokument HTML), a minimalna długość ścieżki renderowania to 1 przebieg w obie strony.