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

Mariko Kosaka

Wewnętrzne działanie procesu renderowania

To jest część 3 z 4-częściowej serii na temat działania przeglądarek. Wcześniej omówiliśmy architekturę wieloprocesową i proces nawigacji. W tym poście przyjrzymy się, co dzieje się w mechanizmie renderowania.

Proces renderowania obejmuje wiele aspektów wydajności sieci. Ze względu na to, że w procesie renderowania wiele się dzieje, ten post jest tylko ogólnym omówieniem tego procesu. Więcej materiałów znajdziesz w sekcji Skuteczność w Podstawach tworzenia witryn.

Procesy mechanizmu renderowania obsługują treści z internetu

Proces renderowania 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. Czasami części kodu JavaScript są obsługiwane przez wątki instancji roboczych, jeśli używasz mechanizmów roboczych witryn roboczych lub service worker. Wątki kompozytora i rastru są również uruchamiane w procesach renderowania, aby sprawnie i płynnie renderować stronę.

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ątku głównym, wątkami instancji roboczych, wątkiem kompozytora i wątkiem rastrowym wewnątrz

Analiza

Konstrukcja DOM

Gdy mechanizm renderowania otrzyma komunikat o zatwierdzeniu na potrzeby nawigacji i zacznie otrzymywać dane HTML, wątek główny rozpoczyna analizowanie ciągu tekstowego (HTML) i przekształca go w konstruowanie (DOM).

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

Przetwarzanie dokumentu HTML na model DOM jest zdefiniowane przez standard HTML. Możesz zauważyć, że przesyłanie kodu HTML do przeglądarki nigdy nie powoduje błędu. Może to być np. brak zamykającego tagu </p> w prawidłowym kodzie HTML. Błędne znaczniki, takie jak Hi! <b>I'm <i>Chrome</b>!</i> (tag b jest zamknięty przed tagiem i), są traktowane tak, jakby tekst Hi! <b>I'm <i>Chrome</i></b><i>!</i> został napisany. Dzieje się tak, ponieważ specyfikacja HTML została zaprojektowana w taki sposób, aby zapewniać obsługę takich błędów. Jeśli ciekawi Cię, jak to się robi, przeczytaj sekcję „Wprowadzenie do obsługi błędów i dziwne przypadki w parserze” specyfikacji HTML.

Wczytuję zasób podrzędny

Witryna zwykle korzysta z zasobów zewnętrznych, takich jak obrazy, CSS i JavaScript. Pliki te muszą być wczytywane z sieci lub pamięci podręcznej. Wątek główny może żądać ich po kolei, gdy je znajdują, gdy są analizowane w celu utworzenia modelu DOM. Aby jednak przyspieszyć działanie, równolegle działa „wstępnie załaduj skaner”. Jeśli w dokumencie HTML znajdują się elementy typu <img> lub <link>, skaner wyświetla tokeny wygenerowane przez parser HTML i wysyła żądania do wątku sieciowego w procesie przeglądarki.

model DOM
Rysunek 2. Analiza kodu HTML w wątku głównym i tworzenie drzewa DOM

JavaScript może blokować analizę

Gdy parser HTML znajdzie tag <script>, wstrzymuje analizę dokumentu HTML oraz musi wczytać, przeanalizować i wykonać kod JavaScript. Dlaczego? Ponieważ JavaScript może zmieniać kształt dokumentu za pomocą elementów takich jak document.write(), co zmienia całą strukturę DOM (omówienie modelu analizy w specyfikacji HTML zawiera ładny diagram). Dlatego parser HTML musi czekać na uruchomienie JavaScriptu, zanim będzie mógł wznowić analizę dokumentu HTML. Jeśli ciekawi Cię, jak wygląda wykonywanie kodu JavaScript, zespół V8 publikuje na ten temat wykłady i posty.

Podpowiedź do przeglądarki, jak chcesz wczytywać zasoby

Programiści stron internetowych mogą wysyłać wskazówki do przeglądarki na wiele sposobów, by poprawnie wczytywać zasoby. Jeśli w Twoim kodzie JavaScript nie jest używany document.write(), możesz dodać atrybut async lub defer do tagu <script>. Następnie przeglądarka wczytuje i uruchamia kod JavaScript asynchronicznie, nie blokuje analizy. W razie potrzeby możesz też skorzystać z modułu JavaScript. <link rel="preload"> to sposób na poinformowanie przeglądarki, że zasób jest zdecydowanie potrzebny do aktualnej nawigacji i że chcesz go jak najszybciej pobrać. Więcej informacji na ten temat znajdziesz w artykule Określanie priorytetów zasobów – pomoc przeglądarce.

Obliczanie stylu

Model DOM nie wystarcza do poznania wyglądu strony, ponieważ możemy określać ich styl w CSS. Wątek główny analizuje kod CSS i określa styl obliczony dla każdego węzła DOM. To informacja o tym, jaki styl jest stosowany do poszczególnych elementów na podstawie selektorów arkusza CSS. Te informacje znajdziesz w sekcji computed w Narzędziach deweloperskich.

Styl wynikowy
Rysunek 3. Analizowanie kodu CSS w wątku głównym w celu dodania stylu obliczeniowego

Nawet jeśli nie podasz żadnego kodu CSS, każdy węzeł DOM ma styl obliczony. Tag <h1> będzie większy niż tag <h2>, a dla każdego elementu zostaną zdefiniowane marginesy. Wynika to z faktu, że przeglądarka ma domyślny arkusz stylów. Jeśli chcesz się dowiedzieć, jaki jest domyślny kod CSS w Chrome, kod źródłowy znajdziesz tutaj.

Układ

Mechanizm renderowania zna już strukturę dokumentu i styl każdego węzła, ale to nie wystarczy, aby wyrenderować stronę. Wyobraź sobie, że próbujesz opisać znajomemu obraz przez telefon. „Duże czerwone koło i mały niebieski kwadrat” to za mało, aby znajomy mógł określić, jak dokładnie będzie wyglądać obraz.

gra o ludzki faks
Rysunek 4. Osoba stojąca przed obrazem, z połączoną linią telefoniczną

Układ to proces znajdowania geometrii elementów. Wątek główny przedstawia DOM i obliczone style oraz tworzy drzewo układu z takimi informacjami 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 parametr display: none, ten element nie będzie częścią drzewa układu (chociaż drzewo układu będzie zawierać element z atrybutem visibility: hidden). Podobnie jeśli zastosowana zostanie pseudoklasa z treścią taką jak p::before{content:"Hi!"}, zostanie ona uwzględniona w drzewie układu, mimo że nie ma jej w DOM.

układ : layout (might be used for DTP, web and app design)
Rysunek 5. Wątek główny przechodzący po drzewie DOM z obliczonymi stylami i generowaniem drzewa układu
Rysunek 6: Układ ramki, w której następuje zmiana podziału wiersza z powodu zmiany podziału wiersza

Ustalenie układu strony to nie lada wyzwanie. Nawet najprostszy układ strony, np. blok z góry na dół, musi uwzględniać wielkość czcionki i miejsce podziału wiersza, ponieważ wpływa to na rozmiar i kształt akapitu, a to z kolei wpływa na to, gdzie musi znaleźć się kolejny akapit.

CSS może powodować pływanie elementu na jednej stronie, maskowanie elementu przepełnienia i zmianę kierunku pisania. Wyobraź sobie, że ten etap układu to nie lada zadanie. W Chrome nad układem pracuje cały zespół inżynierów. Jeśli chcesz poznać szczegóły ich pracy, kilka rozmów z BlinkOn Conference jest nagrana i jest dość interesujący.

Barwiony

gra z rysunkami
Rysunek 7. Osoba przed płótnam trzymająca pędzel i zastanawiająca się, czy najpierw narysować okrąg, czy najpierw kwadrat

Zastosowanie DOM, stylu i układu to nadal za mało, aby wyrenderować stronę. Załóżmy, że próbujesz odtworzyć obraz. Znasz rozmiar, kształt i rozmieszczenie elementów, ale nadal musisz zdecydować, w jakiej kolejności je malujesz.

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

błąd kolejności nakładania elementów,
Rysunek 8. Elementy strony wyświetlane w kolejności znaczników HTML, co powoduje niewłaściwie wyrenderowany obraz, ponieważ nie uwzględniono kolejności nakładania elementów

Na tym etapie renderowania główny wątek przechodzi po drzewie układu, aby utworzyć rekordy renderowania. Rekord farby to notatka dotycząca procesu malowania, np. „pierwsze tło, potem tekst, a potem prostokąt”. Jeśli korzystasz z elementu <canvas> za pomocą JavaScriptu, być może znasz już ten proces.

Renderowanie rekordów
Rysunek 9. Wątek główny przechodzący przez drzewo układu i generując rekordy renderowania

Aktualizacja potoku renderowania jest kosztowna

Rysunek 10. Generowanie drzew DOM + styl, układ i drzewa w kolejności ich generowania

Najważniejszą rzeczą do zrozumienia w potoku renderowania jest to, że na każdym etapie do utworzenia nowych danych używany jest wynik poprzedniej operacji. Jeśli np. w drzewie układu coś się zmieni, trzeba ponownie wygenerować kolejność renderowania dla części dokumentu, w których występuje błąd.

Jeśli animujesz elementy, przeglądarka musi uruchamiać te operacje między każdą klatkami. Większość naszych wyświetlaczy odświeża ekran 60 razy na sekundę (60 kl./s). Animacja jest płynna dla ludzkich oczu, gdy przesuwasz elementy po ekranie w każdej klatce. Jeśli jednak w animacji brakuje klatek, na stronie będzie widać „problemy”.

niezgodność z brakującymi klatkami
Rysunek 11. Klatki animacji na osi czasu

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

jage jank by JavaScript
Rysunek 12. Animacja 12. klatki animacji na osi czasu, ale jedna z nich jest blokowana przez JavaScript

Możesz podzielić operację JavaScriptu na małe fragmenty i zaplanować uruchamianie w każdej klatce za pomocą requestAnimationFrame(). Więcej informacji na ten temat znajdziesz w artykule Optymalizowanie wykonywania JavaScriptu. Możesz też uruchomić JavaScript w Web Workers, by uniknąć zablokowania wątku głównego.

żądanie ramki animacji
Rysunek 13: Mniejsze fragmenty kodu JavaScript działające na osi czasu z ramką animacji

Komponowanie

Jak narysować stronę?

Rysunek 14. Animacja procesu naiwnego rastrowania

Skoro 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 nazywamy rasteryzacją.

Być może naiwnym sposobem radzenia sobie z tym problemem jest rastrowanie fragmentów widocznych w widocznym obszarze. Jeśli użytkownik przewinie stronę, następnie przesunie ramkę rastrową i uzupełni brakujące części, stosując rastrowanie. Tak właśnie Chrome poradził z rasteryzacją w chwili premiery. Nowoczesna przeglądarka wykonuje jednak bardziej zaawansowany proces nazywany komponowaniem.

Czym jest komponowanie

Rysunek 15. Animacja procesu komponowania

Komponowanie to technika rozdzielania części strony na warstwy, rasteryzacji ich oddzielnie i tworzenia jako strony w oddzielnym wątku nazywanym wątkiem kompozytora. Jeśli przewija się, warstwy są już zrastrowane, wystarczy skomponować nową klatkę. Animację można uzyskać w ten sam sposób, przesuwając warstwy i skomponując nową klatkę.

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

Podział na warstwy

Aby dowiedzieć się, które elementy muszą znajdować się w poszczególnych warstwach, wątek główny przechodzi przez drzewo układu, aby utworzyć drzewo warstw (ta część nosi nazwę „Aktualizowanie drzewa warstw” w panelu wydajności Narzędzi deweloperskich). Jeśli dla niektórych części strony, które powinny być oddzielone warstwą (np. wysuwane menu boczne), nie są one widoczne, możesz poinformować przeglądarkę, używając atrybutu will-change w CSS.

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

Nakładanie warstw na każdy element może być kuszące, ale komponowanie nadmiarowej liczby warstw może spowolnić działanie niż rasteryzowanie małych części strony w każdej klatce, dlatego ważne jest, aby mierzyć wydajność renderowania aplikacji. Więcej informacji znajdziesz w artykule na temat używania właściwości tylko do kompozytora i zarządzania liczbą warstw.

rastrowe i złożone z wątku głównego;

Po utworzeniu drzewa warstw i określeniu kolejności renderowania wątek główny zatwierdza te informacje w wątku kompozytora. Wątek kompozytora rasteryzuje każdą warstwę. Warstwa może być duża, jak cała długość strony, więc wątek kompozytora dzieli je na kafelki i wysyła każdy kafelek do wątków rastrowania. Wątki rastrowania rasteryzują każdy kafelek i są przechowywane w pamięci GPU.

rastrowe
Rysunek 17. Tworzenie bitmapy kafelków w wątkach rastrowych i wysyłanie ich do GPU

Wątek kompozytora może nadawać priorytet różnym wątkom rastrowania, aby elementy w widocznym obszarze (lub w pobliżu) mogły być najpierw rastrowane. Warstwa ma też wiele kafelków w różnych rozdzielczościach, które pozwalają np. na powiększanie obrazu.

Po rastrowaniu kafelków wątek kompozytora zbiera informacje o kafelkach nazywane czworokątami rysunkowymi, aby utworzyć ramkę kompozytora.

Narysuj czworokąty Zawiera informacje takie jak lokalizacja kafelka w pamięci i miejsce na stronie, w którym należy go narysować z uwzględnieniem jej kompilacji.
Ramka kompozytora Kolekcja czworokątów 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 na potrzeby 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 tworzy kolejną ramkę kompozytora, która ma zostać wysłana do GPU.

komponowanie
Rysunek 18. Wątek kompozytora tworzy ramkę kompilacji. Ramka jest wysyłana do procesu przeglądarki, a następnie do GPU

Zaletą komponowania jest to, że odbywa się bez udziału wątku głównego. Wątek kompozytora nie musi czekać na obliczenie stylu ani wykonanie JavaScriptu. Dlatego komponowanie tylko animacji jest uznawane za najlepsze do zapewnienia płynności działania. Jeśli trzeba ponownie obliczyć układ lub wyrenderowanie, uwzględnia się wątek główny.

Podsumowanie

W tym poście przyglądaliśmy się potokowi renderowania – od analizowania do komponowania. Mam nadzieję, że teraz możesz przeczytać więcej o optymalizacji skuteczności witryny.

W następnym i ostatnim poście z tej serii omówimy bardziej szczegółowo wątek 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 jakieś pytania lub sugestie dotyczące kolejnego posta, chętnie poznam Twoją opinię w sekcji komentarzy poniżej lub na Twitterze – @kosamari.

Dalej: dane wejściowe są przekazywane do kompozytora