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

Mariko Kosaka

Wewnętrzne działanie procesu mechanizmu renderowania

To jest 3 z 4 części postów na blogu poświęconych działaniu przeglądarek. Omówiliśmy już architekturę wieloprocesową i przepływ nawigacji. W tym poście wyjaśnimy, co dzieje się w procesie renderowania.

Proces renderowania wpływa na wiele aspektów wydajności stron. W procesie renderowania wiele się dzieje, więc ten post zawiera tylko ogólne informacje. Jeśli chcesz dowiedzieć się więcej, więcej materiałów znajdziesz w sekcji Skuteczność w Podstawach tworzenia witryn.

Procesy renderowania obsługują treści z internetu

Mechanizm renderowania odpowiada za wszystko, co dzieje się na karcie. W ramach mechanizmu renderowania wątek główny obsługuje większość kodu wysyłanego do użytkownika. Czasami fragmenty kodu JavaScript są obsługiwane przez wątki instancji roboczych, jeśli korzystasz z instancji roboczej lub service worker. Wątki kompozytorów i rastrów są również uruchamiane w ramach mechanizmu renderowania, aby umożliwić sprawne i płynne renderowanie strony.

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

Proces mechanizmu renderowania
Rysunek 1. Proces mechanizmu renderowania z wątkiem głównym, wątkami instancji roboczych, wątkiem kompozytora i wątkiem rastrowania w środku

Analiza

Konstrukcja DOM

Gdy mechanizm renderowania otrzymuje komunikat zatwierdzenia dotyczący nawigacji i zaczyna otrzymywać dane HTML, wątek główny zaczyna analizować ciąg tekstowy (HTML) i zmieniać go w dokument Object (DOM).

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

Analizowanie dokumentu HTML do formatu DOM jest definiowane przez standard HTML. Jak widzisz, przesłanie kodu HTML do przeglądarki nigdy nie powoduje błędu. Na przykład brak zamykającego tagu </p> jest prawidłowym kodem HTML. Błędne znaczniki, np. Hi! <b>I'm <i>Chrome</b>!</i> (tag b zostaje zamknięty przed tagiem „i”) jest traktowany tak, jakby ciąg znaków Hi! <b>I'm <i>Chrome</i></b><i>!</i> został utworzony przez Ciebie. Wynika to z faktu, że specyfikacja HTML jest zaprojektowana tak, by poprawnie obsługiwała te błędy. Jeśli ciekawi Cię, jak przebiega te procesy, przeczytaj sekcję „Wprowadzenie do obsługi błędów i dziwne przypadki w parserze” w specyfikacji HTML.

Wczytuję zasób podrzędny

Strona internetowa zwykle korzysta z zasobów zewnętrznych, takich jak obrazy, CSS i JavaScript. Pliki te muszą zostać załadowane z sieci lub pamięci podręcznej. Wątek główny może wysyłać żądania dotyczące tych plików po kolei w miarę ich znajdowania podczas analizy w celu utworzenia DOM. Jednak dla przyspieszenia „skaner wstępnego załadowania” uruchamia się równocześnie. Jeśli dokument HTML zawiera takie elementy jak <img> lub <link>, skaner wstępnie wczytuje tokeny wygenerowane przez parser HTML i wysyła żądania do wątku sieciowego w procesie przeglądarki.

DOM
Rysunek 2. Wątek główny analizuje kod HTML i tworzy drzewo DOM

JavaScript może blokować analizę

Gdy parser HTML znajdzie tag <script>, wstrzymuje analizę dokumentu HTML i musi wczytać, przeanalizować oraz wykonać kod JavaScript. Dlaczego? Ponieważ JavaScript może zmieniać kształt dokumentu na przykład za pomocą elementu document.write(), który zmienia całą strukturę DOM (omówienie modelu analizy w specyfikacji HTML ma ładny diagram). Dlatego parser HTML musi poczekać na uruchomienie JavaScriptu, zanim będzie mógł wznowić analizę dokumentu HTML. Jeśli ciekawi Cię, co dzieje się podczas wykonywania JavaScriptu, zespół V8 z odpowiednią stroną omawia ten temat.

Wskazówka dotycząca sposobu ładowania zasobów w przeglądarce

Programiści stron internetowych mogą wysyłać wskazówki do przeglądarki na wiele sposobów, by sprawnie ładować zasoby. Jeśli JavaScript nie używa document.write(), możesz dodać atrybut async lub defer do tagu <script>. Następnie przeglądarka wczytuje i uruchamia kod JavaScript asynchronicznie bez blokowania analizy. W razie potrzeby możesz też użyć modułu JavaScript. <link rel="preload"> to sposób poinformowania przeglądarki, że zasób jest niezbędny do przeprowadzenia nawigacji i że chcesz jak najszybciej pobrać ten zasób. Więcej informacji na ten temat znajdziesz w artykule Określanie priorytetów zasobów – korzystanie z przeglądarki w potrzebie.

Obliczanie stylu

DOM to nie wystarczy, aby wiedzieć, jak będzie wyglądać strona, bo możemy określać style jej elementów w CSS. Wątek główny analizuje kod CSS i określa obliczony styl dla każdego węzła DOM. Są to informacje o stylu stosowanym do poszczególnych elementów na podstawie selektorów arkusza CSS. Te informacje możesz znaleźć w sekcji computed w Narzędziach deweloperskich.

Styl wynikowy
Rysunek 3. Analiza kodu CSS głównego wątku w celu dodania obliczonego stylu

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

Układ

Teraz proces renderowania zna strukturę dokumentu i style każdego węzła, ale to za mało, by wyrenderować stronę. Wyobraź sobie, że próbujesz opisać obraz znajomemu przez telefon. Znajomy „jest duże czerwone koło i mały niebieski kwadrat”, to za mało, aby wiedział, jak dokładnie wygląda obraz.

gra w ludzki faks
Rysunek 4. Osoba stojąca przed obrazem, połączona z rozmówcą linia telefoniczna

Układ to proces znajdowania geometrii elementów. Wątek główny przechodzi przez style DOM i style wynikowe, a następnie tworzy drzewo układu, które zawiera takie informacje jak współrzędne x y i rozmiary ramek ograniczających. Drzewo układu może mieć podobną strukturę do drzewa DOM, ale zawiera tylko informacje związane z tym, co jest widoczne na stronie. Jeśli zastosujesz display: none, element nie będzie częścią drzewa układu (ale element z atrybutem visibility: hidden znajduje się w drzewie układu). I podobnie, jeśli zastosujesz pseudoelement z zawartością taką jak 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. Wątek główny przechodzący przez drzewo DOM z wyliczonymi stylami i tworzący drzewo układu
Rys. 6. Układ ramki akapitu przesuwanego ze względu na zmianę podziału wiersza

Określanie układu strony to niełatwe zadanie. Nawet najprostszy układ strony, np. blok blokowy z góry do dołu, musi brać pod uwagę wielkość czcionki i podział między wierszami. Ma to wpływ na rozmiar i kształt akapitu, a następnie na to, gdzie musi znaleźć się następny akapit.

CSS może sprawić, że element będzie się przesuwał na jedną stronę, maskować przepełniony element i zmieniać kierunki pisania. Jak widać, na tym etapie układu jest trudne zadanie. W Chrome nad układem pracuje cały zespół inżynierów. Jeśli chcesz poznać szczegóły ich pracy, kilka przemówień z konferencji BlinkOn jest nagranych i ciekawych do obejrzenia.

Barwiony

gra w rysowanie
Rysunek 7. Osoba przed płótnem trzymająca pędzel i zastanawia się, czy najpierw narysować okrąg, czy kwadrat

DOM, styl i układ to nadal nie wystarczy, aby wyrenderować stronę. Załóżmy, że próbujesz odtworzyć obraz. Wiesz, jaki jest rozmiar, kształt i rozmieszczenie elementów, ale i tak musisz ocenić, w jakiej kolejności je malować.

Na przykład dla niektórych elementów można ustawić atrybut z-index. W takim przypadku malowanie elementów w kolejności elementów zapisanych w kodzie HTML spowoduje nieprawidłowe renderowanie.

błąd kolejności nakładania elementów
Rysunek 8. Rysunek 8. Elementy strony pojawiają się w kolejności po znacznikach HTML, powodując nieprawidłowe wyrenderowanie obrazu (z powodu braku uwzględnienia wartości z-index)

Na tym etapie malowania wątek główny przechodzi po drzewie układu, aby utworzyć rekordy renderowania. Rekord malowania to uwaga dotycząca procesu malowania, np. „najpierw tło, potem tekst, a następnie prostokąt”. Jeśli narysowałeś(-aś) element <canvas> za pomocą JavaScriptu, ten proces może być Ci znany.

maluj rekordy
Rysunek 9. Wątek główny przechodzi przez drzewo układu i generuje rekordy malowania

Aktualizowanie potoku renderowania jest kosztowne

Rysunek 10. Drzewa DOM+Style, Układ i Malowanie w kolejności ich generowania

Najważniejszą rzeczą do zapamiętania w ramach potoku renderowania jest to, że do tworzenia nowych danych na każdym etapie wykorzystywana jest wynik poprzedniej operacji. Jeśli np. coś się zmieni w drzewie układu, musisz ponownie wygenerować kolejność Renderowania w przypadku odpowiednich części dokumentu.

Jeśli animujesz elementy, przeglądarka musi przeprowadzać te operacje między każdą klatką. Większość naszych wyświetlaczy odświeża ekran 60 razy na sekundę (60 kl./s). Gdy przesuwasz elementy na ekranie w każdej klatce, animacja jest płynna. Jeśli jednak animacja brakuje klatek między klatkami, strona będzie wyglądała niestabilnie.

zacinanie się reklam z powodu brakujących ramek
Rysunek 11. Klatki animacji na osi czasu

Nawet jeśli operacje renderowania przebiegają zgodnie z odświeżaniem ekranu, obliczenia są wykonywane w wątku głównym, co oznacza, że mogą być blokowane, gdy aplikacja używa JavaScriptu.

JavaScript jank
Rysunek 12. Klatki animacji na osi czasu, ale jedna z nich jest blokowana przez kod JavaScript

Możesz podzielić operację JavaScript na małe fragmenty i zaplanować uruchamianie jej w każdej klatce za pomocą polecenia requestAnimationFrame(). Więcej informacji na ten temat znajdziesz w artykule o optymalizacji wykonywania JavaScriptu. Aby uniknąć blokowania wątku głównego, możesz też uruchomić JavaScript w narzędziach Web Workers.

żądanie ramki animacji
Rysunek 13. Mniejsze fragmenty kodu JavaScript uruchomionego na osi czasu z klatką animacji

Komponowanie

Jak narysować stronę?

Rysunek 14. Animacja procesu naiwnego rastrowania

Przeglądarka zna już strukturę dokumentu, styl poszczególnych elementów, geometrię strony i kolejność renderowania. W jaki sposób może to zrobić? Przekształcanie tych informacji w piksele na ekranie nazywamy rasteryzacją.

Najlepszym sposobem na poradzenie sobie z tym problemem jest rastrowanie części wewnątrz widocznego obszaru. Jeśli użytkownik przewinie stronę, a potem przesunie ramkę z rastrem i uzupełnia brakujące elementy, wykorzystując rastrowanie. W ten sposób przeglądarka Chrome obsługiwała rasteryzację w chwili jej wydania. Nowoczesna przeglądarka wykonuje bardziej zaawansowany proces nazywany komponowaniem.

Co to jest komponowanie

Rysunek 15. Animacja procesu komponowania

Komponowanie to technika polegająca na rozdzieleniu części strony na warstwy, zrasteryzowaniu ich z osobna oraz jako skomponowaniu jako strony w osobnym wątku nazywanym wątkiem kompozytora. Jeśli występuje przewijanie, ponieważ warstwy są już zrasteryzowane, wystarczy, że skomponujesz nową ramkę. Animację można uzyskać w ten sam sposób, przenosząc warstwy i skomponując nową klatkę.

Aby sprawdzić, jak Twoja witryna jest podzielona na warstwy, możesz użyć panelu Warstwy w Narzędziach deweloperskich.

Podział na warstwy

Aby dowiedzieć się, które elementy powinny znajdować się w poszczególnych warstwach, wątek główny przechodzi przez drzewo układów, aby utworzyć drzewo warstw (ta część nosi nazwę „Aktualizuj drzewo warstw” w panelu wydajności Narzędzi deweloperskich). Jeśli niektóre części strony, które powinny stanowić osobną warstwę (np. wysuwane menu boczne), nie otrzymują żadnej, możesz wskazać przeglądarce, używając atrybutu will-change w CSS.

drzewo warstw
Rysunek 16. Wątek główny przechodzi przez drzewo układu tworzące drzewo warstw

Dodawanie warstw do każdego elementu może wydawać się kuszące, ale komponowanie zbyt wielu warstw może spowalniać działanie niż rasteryzacja małych części strony w każdej klatce, dlatego ważne jest, aby zmierzyć wydajność renderowania aplikacji. Więcej informacji na ten temat znajdziesz w artykule Zachowaj właściwości tylko z kompozytu i zarządzanie liczbą warstw.

Rastrowe i złożone poza wątkiem głównym

Po utworzeniu drzewa warstw i określeniu zamówień renderowania w wątku głównym wątki te zatwierdzają te informacje w wątku kompozytora. Wątek kompozytora rasteryzuje następnie każdą warstwę. Warstwa może być duża jak cała strona, więc wątek kompozytora dzieli je na kafelki i wysyła każdy z nich do wątków rastrów. Wątki rastrowania rastrują każdy kafelek i zapisują je w pamięci GPU.

rastrowe
Rysunek 17. Wątki rastrowania tworzące bitmapę kafelków i wysyłające je do GPU

Wątek kompozytora może nadać priorytet różnym wątkom rastrowania, tak aby najpierw można było umieścić w widocznym obszarze (lub w pobliżu) obiekty w widocznym obszarze. Warstwa zawiera też wiele kafelków dla różnych rozdzielczości, co pozwala obsługiwać takie aspekty jak powiększanie.

Po zrastrowaniu kafelków wątek kompozytora gromadzi informacje o kafelkach nazywane draw quad, aby utworzyć ramkę kompozytora.

Narysuj czworokąty Zawiera informacje takie jak lokalizacja kafelka w pamięci i miejsce na stronie, aby go narysować z uwzględnieniem komponowania strony.
Ramka kompozytora Zbiór czworokątów do rysowania reprezentujących ramkę strony.

Ramka kompozytora jest następnie przesyłana do procesu przeglądarki przez IPC. W tym momencie można dodać kolejną ramkę kompozytora z wątku UI w celu zmiany interfejsu przeglądarki lub z innych procesów renderowania rozszerzeń. Te ramki kompozytora są wysyłane do GPU w celu wyświetlenia ich na ekranie. Jeśli pojawi się zdarzenie przewijania, wątek kompozytora utworzy kolejną ramkę kompozytora, która zostanie wysłana do GPU.

kompozycja
Rysunek 18. Wątek kompozytora tworzący ramkę komponującą. Ramka jest wysyłana do procesu przeglądarki, a następnie do GPU.

Zaletą komponowania jest to, że odbywa się to bez udziału wątku głównego. Wątek kompozytora nie musi czekać na obliczenie stylu ani wykonanie JavaScriptu. Dlatego komponowanie tylko animacji jest uważane za najlepsze, jeśli zależy Ci na płynności działania. Jeśli trzeba ponownie obliczyć wartości układu lub renderowania, należy uwzględnić wątek główny.

Podsumowanie

W tym poście omówiliśmy proces renderowania w potoku renderowania od analizy do komponowania. Mamy nadzieję, że teraz masz więcej informacji o optymalizacji witryny.

W następnym i ostatnim poście z tej serii bardziej szczegółowo przyjrzymy się wątekowi kompozytora i zobaczymy, co się stanie, gdy pojawią się dane wejściowe użytkownika, takie jak mouse move i click.

Podobał Ci się post? Jeśli masz pytania lub sugestie związane z przyszłym postem, skontaktuj się z nami w sekcji komentarzy poniżej lub napisz na adres @kosamari na Twitterze.

Dalej: dane wejściowe będą przesyłane do kompozytora