Nowoczesna przeglądarka internetowa (część 3)

Mariko Kosaka

Zasady działania procesu renderowania

To jest trzeci z 4 postów w serii blogowej poświęconej działaniu przeglądarek. W poprzednich artykułach omawialiśmy architekturę wieloprocesowąprzepływ nawigacji. W tym poście omówimy, co dzieje się w procesie renderowania.

Proces przetwarzania przez procesor graficzny wpływa na wiele aspektów wydajności stron internetowych. Ponieważ w ramach procesu przetwarzania dzieje się wiele, ten post jest tylko ogólnym przeglądem. Jeśli chcesz dowiedzieć się więcej, w sekcji Skuteczność w artykule Podstawy internetu znajdziesz wiele dodatkowych materiałów.

Procesy renderowania obsługują treści internetowe

Proces renderera odpowiada za wszystko, co dzieje się na karcie. W procesie renderowania wątek główny obsługuje większość kodu wysyłanego do użytkownika. Jeśli używasz skryptu web worker lub skryptu service worker, czasami niektóre fragmenty kodu JavaScript są obsługiwane przez wątki robocze. Przetwornik i wątki rastrowe są też wykonywane w procesach renderowania, aby renderowanie strony było wydajne i płynne.

Głównym zadaniem procesu renderowania jest przekształcanie kodu HTML, CSS i JavaScript w stronę internetową, z którą użytkownik może wchodzić w interakcję.

Proces renderowania
W ramach procesu renderowania: wątek główny, wątki robocze, wątek kompozytora i wątek rastrowania

Analizowanie

Budowa DOM

Gdy proces renderowania otrzyma wiadomość o zmianie dotyczącą nawigacji i zacznie otrzymywać dane HTML, główny wątek zacznie analizować ciąg tekstowy (HTML) i przekształcać go w Dokument Object Model (DOM).

DOM to wewnętrzna reprezentacja strony w przeglądarce, a także struktura danych i interfejs API, z którymi programista może wchodzić w interakcję za pomocą JavaScriptu.

Analiza dokumentu HTML na model DOM jest zdefiniowana przez standard HTML. Zauważysz, że przesyłanie kodu HTML do przeglądarki nigdy nie powoduje błędu. Na przykład brakujący tag zamykający </p> jest prawidłowy w HTML. Błędny znacznik, taki jak Hi! <b>I'm <i>Chrome</b>!</i> (tag b jest zamknięty przed tagiem i), jest traktowany tak, jakbyś napisał Hi! <b>I'm <i>Chrome</i></b><i>!</i>. Dzieje się tak, ponieważ specyfikacja HTML została zaprojektowana tak, aby odpowiednio obsługiwać takie błędy. Jeśli chcesz się dowiedzieć, jak to działa, przeczytaj sekcję „Wprowadzenie do obsługi błędów i nietypowych przypadków w parsowaniu” w specyfikacji HTML.

Wczytywanie zasobu podrzędnego

Strona internetowa zwykle korzysta z zasobów zewnętrznych, takich jak obrazy, CSS i JavaScript. Pliki te muszą zostać pobrane z sieci lub pamięci podręcznej. Główny wątek może żądać ich pojedynczo, gdy je znajdzie, podczas analizowania, aby utworzyć DOM, ale aby przyspieszyć, „skaner wstępnego wczytania” jest uruchamiany równolegle. Jeśli w dokumencie HTML znajdują się elementy takie jak <img> lub <link>, skaner wstępnego wczytywania sprawdza tokeny wygenerowane przez parsowanie HTML i wysyła żądania do wątku sieciowego w procesie przeglądarki.

DOM
Ryc. 2. Główny wątek analizuje kod HTML i tworzy drzewo DOM

Kod JavaScript może blokować analizowanie.

Gdy analizator HTML znajdzie tag <script>, wstrzymuje analizowanie dokumentu HTML i musi załadować, przeanalizować i wykonać kod JavaScript. Dlaczego? Ponieważ JavaScript może zmieniać kształt dokumentu za pomocą takich elementów jak document.write(), które zmieniają całą strukturę DOM (omówienie modelu analizy – specyfikacja HTML zawiera odpowiedni diagram). Dlatego parsujący HTML musi poczekać, aż JavaScript się wykona, zanim wznowi analizowanie dokumentu HTML. Jeśli chcesz się dowiedzieć, co dzieje się podczas wykonywania kodu JavaScript, przeczytaj wpisy na blogu i wysłuchaj rozmów na ten temat z zespołem V8.

Podpowiedz przeglądarce, jak chcesz wczytywać zasoby

Deweloperzy mogą wysyłać do przeglądarki wskazówki na temat ładowania zasobów na wiele sposobów. Jeśli kod JavaScript nie używa tagu document.write(), możesz dodać do tagu <script> atrybut async lub defer. Następnie przeglądarka asynchronicznie wczytuje i uruchamia kod JavaScriptu, nie blokując przy tym analizowania. Jeśli to możliwe, możesz też użyć modułu JavaScriptu. <link rel="preload"> to sposób na poinformowanie przeglądarki, że zasób jest zdecydowanie potrzebny do bieżącej nawigacji i chcesz go pobrać tak szybko, jak to możliwe. Więcej informacji na ten temat znajdziesz w artykule Priorytetyzacja zasobów – jak sprawić, aby przeglądarka Ci w tym pomogła.

Obliczanie stylu

Interfejs DOM nie wystarczy, aby określić, jak będzie wyglądać strona, ponieważ elementy strony możemy stylizować za pomocą kodu CSS. Główny wątek analizuje CSS i określa obliczone style dla każdego węzła DOM. Są to informacje o tym, jaki styl jest stosowany do każdego elementu na podstawie selektorów arkusza CSS. Te informacje znajdziesz w sekcji computed w Narzędziach deweloperskich.

Styl wynikowy
Rysunek 3. Główny wątek analizujący CSS w celu dodania obliczonego stylu

Nawet jeśli nie podasz żadnych wartości CSS, każdy węzeł DOM ma obliczony styl. Tag <h1> jest wyświetlany większy niż tag <h2>, a marginesy są zdefiniowane dla każdego elementu. Dzieje się tak, ponieważ przeglądarka ma domyślny arkusz stylów. Jeśli chcesz zobaczyć, jak wygląda domyślny kod CSS w Chrome, tutaj możesz zobaczyć kod źródłowy.

Układ

Teraz proces renderowania zna strukturę dokumentu i style poszczególnych węzłów, ale to nie wystarczy do wyrenderowania strony. Wyobraź sobie, że próbujesz opisać znajomemu obraz przez telefon. „Na obrazie jest duży czerwony okrąg i mały niebieski kwadrat” to za mało informacji, aby Twój przyjaciel wiedział, jak dokładnie wygląda obraz.

gra w faks
Rysunek 4. Osoba stojąca przed obrazem, połączona z inną osobą przez linię telefoniczną

Układ to proces znajdowania geometrii elementów. Główny wątek przechodzi przez DOM i obliczone style, tworząc drzewo układu, które zawiera informacje takie jak współrzędne x y i wymiary ograniczające. Drzewo układu może mieć podobną strukturę jak drzewo DOM, ale zawiera tylko informacje dotyczące tego, co jest widoczne na stronie. Jeśli zastosowano wartość display: none, element nie jest częścią drzewa układu (element z wartością visibility: hidden znajduje się jednak w drzewie układu). Podobnie jeśli zastosujesz pseudoelement z treścią, np. p::before{content:"Hi!"}, zostanie on uwzględniony w drzewie układu, mimo że nie znajduje się w DOM.

układ : layout (might be used for DTP, web and app design)
Rysunek 5. Główny wątek przechodzi po drzewie DOM z obliczonymi stylami i tworzy drzewo układu
Rysunek 6. Układ pola dla akapitu przesuwanego z powodu zmiany znaku końca wiersza

Określanie układu strony jest trudnym zadaniem. Nawet przy najprostszym układzie strony, takim jak blok tekstu od góry do dołu, trzeba wziąć pod uwagę wielkość czcionki i miejsca dzielenia wierszy, ponieważ wpływa to na rozmiar i kształt akapitu, a to z kolei wpływa na to, gdzie powinien znajdować się następny akapit.

Za pomocą CSS możesz przesunąć element na jedną stronę, zamaskować element przepełnienia i zmienić kierunek pisania. Jak możesz sobie wyobrazić, ten etap układu ma ogromne znaczenie. W Chrome nad układem pracuje cały zespół inżynierów. Jeśli chcesz dowiedzieć się więcej o ich pracy, możesz obejrzeć kilka wystąpień z konferencji BlinkOn.

Barwiony

gra w rysowanie
Ryc. 7. Osoba stojąca przed płótnem i trzymająca pędzel, zastanawiająca się, czy najpierw narysować koło czy kwadrat

DOM, styl i układ to za mało, aby renderować stronę. Załóżmy, że chcesz odtworzyć obraz. Znasz rozmiar, kształt i położenie elementów, ale nadal musisz zdecydować, w jakiej kolejności je namalować.

Na przykład w przypadku niektórych elementów może być ustawiona wartość z-index. W takim przypadku wypełnianie w kolejności elementów zapisanych w HTML spowoduje nieprawidłowe renderowanie.

z-index fail
Ryc. 8. Elementy strony pojawiające się w kolejności znaczników HTML, co powoduje błędne renderowanie obrazu, ponieważ nie uwzględniono indeksu z-index

Na tym etapie rysowania wątek główny przechodzi przez drzewo układu, aby utworzyć rekordy rysowania. Rekord malowania to nota o procesie malowania, np. „Najpierw tło, potem tekst, potem prostokąt”. Jeśli korzystasz z rysowania na elemencie <canvas> za pomocą JavaScriptu, ten proces może Ci być znajomy.

rekordy malowania
Rysunek 9. Główny wątek przechodzi przez drzewo układu i tworzy rekordy malowania

Aktualizacja potoku renderowania jest kosztowna

Ryc. 10. Drzewa DOM+Style, Layout i Paint w kolejności generowania

Najważniejszą rzeczą, którą należy zrozumieć w pipeline renderowania, jest to, że na każdym etapie do tworzenia nowych danych służy wynik poprzedniej operacji. Jeśli na przykład coś zmieni się w drzewie układu, zamówienie w Paint musi zostać wygenerowane ponownie w przypadku dotkniętych części dokumentu.

Jeśli animujesz elementy, przeglądarka musi wykonywać te operacje między każdą klatką. Większość naszych wyświetlaczy odświeża ekran 60 razy na sekundę (60 FPS); animacja będzie płynna dla ludzkiego oka, gdy będziesz przemieszczać elementy na ekranie w każdej klatce. Jeśli jednak animacja pominie jakieś klatki, strona będzie wyglądać nienaturalnie.

jage jank by missing frames
Rysunek 11. Ramki animacji na osi czasu

Nawet jeśli operacje renderowania nadążają za odświeżaniem ekranu, te obliczenia są wykonywane w wątku głównym, co oznacza, że mogą być blokowane, gdy aplikacja uruchamia JavaScript.

jage jank by JavaScript
Ryc. 12. Kadry animacji na osi czasu, ale jeden z nich jest zablokowany przez JavaScript

Za pomocą requestAnimationFrame() możesz podzielić działanie JavaScriptu na małe fragmenty i zaplanować jego wykonywanie w każdej klatce. Więcej informacji na ten temat znajdziesz w artykule Optymalizacja wykonywania kodu JavaScript. Możesz też uruchamiać skrypty JavaScriptu w elementach Web Worker, aby uniknąć blokowania wątku głównego.

request animation frame
Ryc. 13. Mniejsze fragmenty kodu JavaScriptu działające na osi czasu z klatką animacji

Kompilowanie

Jak narysujesz stronę?

Ryc. 14. Animacja procesu prostego rastrowania

Teraz, gdy przeglądarka zna już strukturę dokumentu, styl każdego elementu, geometrię strony i kolejność renderowania, jak rysuje stronę? Przekształcanie tych informacji w piksele na ekranie nazywa się rasteryzacją.

Naiwnym sposobem na rozwiązanie tego problemu byłoby rasteryzowanie części widocznych w widocznym obszarze. Jeśli użytkownik przewinie stronę, przesunie rasterowany kadr i wypełni brakujące części, rasterując je ponownie. W ten sposób Chrome obsługiwał rasteryzację w momencie premiery. Nowoczesne przeglądarki wykonują jednak bardziej zaawansowany proces zwany kompozycją.

Co to jest kompozycja

Ryc. 15. Animacja procesu tworzenia kompozycji

Kompozytowanie to technika polegająca na rozdzielaniu części strony na warstwy, rasteryzowaniu ich osobno i kompletowaniu jako strony w osobnym wątku, zwanym wątkiem kompozytora. Jeśli nastąpi przewijanie, warstwy są już zarasteryzowane, więc wystarczy złożyć nowy kadr. Animację można uzyskać w ten sam sposób, przesuwając warstwy i kompilując nową klatkę.

W Narzędziach deweloperskich możesz zobaczyć, jak Twoja witryna jest podzielona na warstwy, korzystając z panelu Warstwy.

Dzielenie na warstwy

Aby ustalić, które elementy powinny znajdować się na których warstwach, główny wątek przechodzi przez drzewo układu, aby utworzyć drzewo warstw (ta część nazywa się „Aktualizuj drzewo warstw” w panelu wydajności DevTools). Jeśli niektóre części strony, które powinny być oddzielną warstwą (np. wysuwane menu boczne), nie są oddzielne, możesz zasugerować to przeglądarce, używając atrybutu will-change w CSS.

drzewo warstw
Rysunek 16. Główny wątek przechodzi przez drzewo układu, tworząc drzewo warstw

Możesz mieć pokusę, aby stosować warstwy do każdego elementu, ale kompozytowanie na zbyt dużej liczbie warstw może spowodować wolniejsze działanie niż rastrowy odczyt małych części strony w każdej klatce. Dlatego bardzo ważne jest, aby mierzyć wydajność renderowania aplikacji. Więcej informacji na ten temat znajdziesz w artykule Trzymaj się właściwości tylko dla kompozytora i zarządzaj liczbą warstw.

Rastryzowanie i kompozycja poza wątkiem głównym

Po utworzeniu drzewa warstw i określeniu kolejności malowania główny wątek przekazuje te informacje wątkowi kompozytora. Następnie wątek kompozytora rastrowuje poszczególne warstwy. Warstwy mogą być duże, np. równe całej długości strony, więc wątek kompozytora dzieli je na elementy i przesyła je do wątków rastrowych. Wątek rastrowy rastrowuje każdą płytkę i przechowuje ją w pamięci GPU.

raster
Ryc. 17. Wątki rastrowe tworzące bitmapę płytek i wysyłające ją do GPU

Wątek kompozytora może nadawać priorytety różnym wątkom rastrowania, aby elementy w widoku (lub w pobliżu) mogły zostać zarasteryzowane jako pierwsze. Warstwy mają też wiele wersji dla różnych rozdzielczości, aby obsługiwać takie działania jak przybliżanie.

Gdy elementy zostaną przetworzone na raster, wątek kompozytora gromadzi informacje o elementach, zwane kwadratami rysowania, aby utworzyć ramkę kompozytora.

Rysowanie kwadratów Zawiera informacje takie jak lokalizacja kafelka w pamięci i miejsce na stronie, w którym ma być wyświetlony kafelek, z uwzględnieniem składania strony.
Ramka w kompozytorze Kolekcja kwadratów rysowania reprezentujących ramkę strony.

Następnie do procesu przeglądarki przesyłany jest element kompozytora za pomocą interfejsu IPC. W tym momencie do wątku interfejsu użytkownika można dodać kolejny element kompozytora, aby wprowadzić zmianę w interfejsie przeglądarki, lub z innych procesów renderowania, aby wprowadzić zmianę w rozszerzeniach. Te klatki kompozytora są wysyłane do procesora graficznego, aby wyświetlić je na ekranie. Jeśli pojawi się zdarzenie przewijania, wątek kompozytora utworzy kolejny kadr kompozytora, który zostanie wysłany do GPU.

composit
Rysunek 18. Wątek kompozytora tworzący ramkę kompozytowania. Ramka jest wysyłana do procesu przeglądarki, a następnie do procesora graficznego.

Zaletą kompozytowania jest to, że odbywa się ono bez udziału wątku głównego. Wątek kompozytora nie musi czekać na obliczenie stylu ani wykonanie kodu JavaScript. Dlatego tylko animacje złożone są uważane za najlepsze rozwiązanie zapewniające płynne działanie. Jeśli układ lub malowanie muszą zostać ponownie obliczone, musi być zaangażowany główny wątek.

Podsumowanie

W tym poście omówiliśmy proces renderowania od analizy do komponowania. Mamy nadzieję, że wiesz już więcej o optymalizacji wydajności witryny.

W następnym i ostatnim poście z tej serii przyjrzymy się bliżej wątkowi kompozytora i zobaczymy, co się dzieje, gdy użytkownik wprowadzi dane wejściowe, takie jak mouse moveclick.

Czy spodobał Ci się ten post? Jeśli masz pytania lub sugestie dotyczące przyszłego wpisu, daj nam znać w sekcji komentarzy poniżej lub na Twitterze, pisząc do @kosamari.

Dalej: dane wejściowe trafiają do kompozytora