Data publikacji: 31 marca 2014 r.
Aby wykrywać i rozwiązywać problemy z wydajnością na kluczowych etapach ścieżki 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 JavaScriptu. 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 reprezentuje czas oczekiwania przeglądarki w sieci bez otrzymywania żadnych bajtów odpowiedzi, a część pełna pokazuje czas potrzebny na zakończenie pobierania po otrzymaniu pierwszych bajtów odpowiedzi. Plik HTML do pobrania jest niewielki (mniej niż 4 KB), więc wystarczy nam 1 przesyłanie w obie strony, aby pobrać cały plik. 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.
Gdy treść HTML stanie się dostępna, przeglądarka przeanalizuje bajty, przekształci je w tokeny i utworzy drzewo DOM. Zauważ, że DevTools wygodnie podaje czas zdarzenia DOMContentLoaded u dołu (216 ms), który odpowiada 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. W zasadzie, gdy mówimy o krytycznej ścieżce renderowania, mamy na myśli znaczniki HTML, CSS i JavaScript. Obrazy nie blokują początkowego renderowania strony, ale powinniśmy też postarać się, aby obrazy były renderowane jak najszybciej.
Zdarzenie load
(znane też jako onload
) jest jednak zablokowane na obrazie: Narzędzie deweloperów odnotowuje zdarzenie onload
o długości 335 ms. Pamiętaj, że zdarzenie onload
oznacza punkt, 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 na wykresie kaskadowym).
Dodawanie kodu 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ż samego kodu HTML: prawdopodobnie będziemy mieć arkusz stylów CSS i co najmniej jeden skrypt, aby dodać stronie interaktywność. Dodaj oba te elementy, aby sprawdzić, 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>
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 przykładu zwykłego kodu HTML musimy też pobrać i przeanalizować plik CSS, aby zbudować obiekt CSSOM, a do zbudowania drzewa renderowania potrzebujemy zarówno obiektu DOM, jak i obiektu 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 obiekt CSSOM. Krótko mówiąc, wbudowany JavaScript również blokuje parser.
Czy mimo blokowania przez CSS umieszczenie skryptu w głównym kodzie strony przyspieszy renderowanie? 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 kod JavaScript jest wbudowany, czy zewnętrzny, ponieważ gdy tylko przeglądarka natrafi na tag skryptu, blokuje go i czeka, aż zostanie utworzony obiekt 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 skryptów blokujących parsowanie, budowa obiektu CSSOM może przebiegać 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>
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 z drugiej strony przeglądarka nie będzie musiała czekać na pobranie żadnych zasobów zewnętrznych, ponieważ 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 uniwersalnego rozwiązania tego problemu, ponieważ każda strona jest inna. Aby znaleźć optymalną strategię, musisz samodzielnie wykonać podobny proces.
Zobaczmy, czy uda nam się zidentyfikować ogólne wzorce wydajności.
Wzorce skutecznoś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 i T1 obejmuje czas przetwarzania przez sieć i serwer. W najlepszym przypadku (jeśli plik HTML jest mały) cały dokument jest pobierany tylko raz. 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>
Ponownie uruchamiamy pętlę sieciową, aby pobrać dokument HTML, a potem pobrany znacznik informuje nas, że potrzebujemy też pliku CSS. Oznacza to, że przeglądarka musi wrócić na serwer i pobrać plik CSS, zanim będzie mogła wyrenderować stronę na ekranie. W rezultacie ta strona wymaga co najmniej 2 przesłań w obie strony, zanim będzie mogła się wyświetlić. 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 opisu krytycznej ścieżki renderowania:
- Critical Resource (Critical Resource):zasób, który może blokować 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. Pierwsze z nich, zawierające jedną stronę HTML, miało jeden 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.
Porównaj to z charakterystyką ścieżki krytycznej w poprzednim przykładzie kodu HTML i CSS:
- 2 krytyczne 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 rezultacie zarówno HTML, jak i CSS są zasobami krytycznymi: pliki CSS są pobierane dopiero po pobraniu dokumentu HTML, dlatego długość ścieżki krytycznej to co najmniej 2 przesyłanie 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.
Jeśli jednak przyjrzymy się „siećom kaskadowym” 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 rezultacie strona widoczna na poprzednim obrazie ma następujące 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 cech ścieżki renderowania oznacza możliwość identyfikowania kluczowych zasobów i zrozumienia, jak przeglądarka będzie planować 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. Zawiera on 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ż parsowania i nie jest częścią ścieżki renderowania krytycznego.
- Ponieważ nie ma innych krytycznych skryptów, skrypt CSS nie musi blokować zdarzenia
domContentLoaded
. - Im szybciej zostanie wywołane zdarzenie
domContentLoaded
, tym szybciej może się rozpocząć wykonywanie innej logiki aplikacji.
W rezultacie nasza zoptymalizowana strona znów zawiera 2 kluczowe zasoby (HTML i CSS), a minimalna długość ścieżki krytycznej to 2 przesyłanie w obie strony, co daje w sumie 9 KB danych krytycznych.
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 rezultacie ta strona ma tylko 1 zasób krytyczny (dokument HTML), a minimalna długość ścieżki renderowania krytycznego to 1 przesył.