Bulldogjob pl pattern 530x244 002
DataArt działa od 1997 roku. Zajmuje się konsultingiem i outsourcingiem usług IT. Jej polskie biura zlokalizowane są w Lublinie i Wrocławiu.

Artykuł powstał na bazie webinarium, które przeprowadziłem jakiś czas temu. Skierowany jest do programistów, który chcą zgłębić wiedzę w zakresie działania przeglądarek, albo mają luki w dotychczas zdobytej wiedzy. Prawdopodobnie jest pełen oczywistości dla tych, którzy zaczęli swoją przygodę z web developmentem. Artykuł podzieliłem na dwie części – w pierwszej przyjrzymy się ogólnym zasadom działania przeglądarek, w drugiej – wyjaśnię pewne istotne procesy i terminologię taką jak reflow, repaint oraz event loop.

 

Czym jest przeglądarka?

Przeglądarka to aplikacja działająca w danym systemie operacyjnym. Znacząca większość przeglądarek jest napisana w C++. Głównym zadaniem takich aplikacji jest prezentowanie zasobów zamieszczonych w sieci na podstawie zapytań wysłanych do serwera oraz wyświetlenie ich w oknie przeglądarki.  zasoby te to zazwyczaj strony HTML, ale mogą być to także pliki PDF, PNG, JPG albo XML, a także różne inne typy zawartości. Dla użytkowników dostępne są różne przeglądarki, a te najpopularniejsze to: Chrome, Internet Explorer, Firefox, Safari i Opera. Przedyskutuję również przeglądarki open source – Chrome, Firefox i Safari.

Z czego składa się i jak działa przeglądarka?

Obraz pokazuje moduły przeglądarki, a każdy z nich ma swoją funkcję. Zacznijmy od interfejsu użytkownika (UI).

UI to te elementy, które użytkownik widzi na ekranie: pasek adresu, przyciski nawigacyjne, menu, itd. Pomimo tego, że interfejsy użytkownika w przeglądarkach mają wiele wspólnego, nie są objęte żadną wspólną specyfikacją. Są owocem dobrych praktyk ukształtowanych latami doświadczenia i rezultatem imitacji siebie nawzajem.

Silnik przeglądarki (browser engine) jest odpowiedzialny za komunikację pomiędzy UI oraz silnikiem renderującym, a także zapis danych.

Silnik renderujący (rendering engine) to najważniejszy dla programistów składnik przeglądarki, ponieważ większość procesów zachodzi właśnie tam. Jest on odpowiedzialny za wyświetlanie żądanych treści na ekranie przeglądarki. Kiedy mówimy o silnikach takich jak Webkit (w Chrome do roku 2013 i Safari) albo Gecko (używanym przez Firefox), po pierwsze myślimy o silniku renderującym. W tym artykule przyjrzymy się mu bliżej, by poznać szczegóły jego pracy.

Warstwa sieciowa (networking) jest odpowiedzialna za połączenia sieciowe, takie jak żądania http, używanie danych pochodzących z zasobów sieciowych oraz interakcję z serwerem renderującym.

Moduł JavaScript Interpreter jest używany do przetwarzania i wykonywania kodu JavaScipt. Istnieje wiele różnych silników JS. Najpopularniejsze to V8 i JavaSriptCore. Ważne, by nie mylić pojęcia silnika przeglądarki z silnikiem JS, który pracuje w ramach modułu JavaScript Interpreter.

Backend UI jest używany do podstawowych widgetów. Ten moduł jest odpowiedzialny za wyświetlanie całej zawartości na ekranie oraz wydajność interfejsu użytkownika.

Ostatnim modułem jest przechowywanie danych. Przeglądarka może potrzebować zapisywania danych na poziomie lokalnym używając RUM (Real User Monitoring). Jakie dane powinny być przechowywyane? Może to być, na przykład, cache albo ustawienia przeglądarki. Takie aplikacje wspierają również mechanizmy przechowywania takie jak IndexedDB, która jest nowym pojęciem w HTML5 – do przechowywania danych wewnątrz przeglądarki użytkownika.

 Silnik renderujący 

Schemat działania:

Analiza HTML i konstrukcja drzewa DOM -> Konstrukcja drzewa renderowania-> Layout drzewa renderowania -> Malowanie drzewa renderowania

 

Silnik renderujący otrzymuje zawartość żądanego dokumentu z warstwy sieciowej w częściach 8 – kilobajtowych. Ważne, by zrozumieć, że silnik wyświetla część zawartości na ekranie tak szybko jak to możliwe, podczas gdy reszta zawartości jest pobierana z sieci. Rezultatem analizy stron HTML (to zupełnie inny, szeroki temat, którego nie będziemy poruszać w tym artykule) jest konstrukcja drzewa DOM. Dodatkowo, zdarzenie load odpala, kiedy analiza jest zakończona. Zdarzenie, które jest wywołane po zakończeniu analizy może być przetworzone w skrypcie. To oznacza, że dokument jest gotowy i skrypt może z nim pracować.

DOM to skrót od Document Object Model. To reprezentacja obiektu dokumentu HTML i interfejsu elementów HTML dla silnika JavaScript. W momencie, kiedy tworzy się drzewo DOM, przeglądarka konstruuje inne drzewo, drzewo renderowania, na jego podstawie. To drzewo jest także istotną częścią silnika renderującego. Ujmując rzecz ogólnie – te dwa drzewa – drzewo DOM i drzewo renderowania – to dwa najważniejsze elementy dla developerów. Drzewo renderowania ma strukturę podobną do drzewa DOM (w dalszej części tekstu pojawi się przykład), ale są między nimi różnice:

  1. Niewizualne elementy DOM nie są umieszczone w drzewie renderowania. Na przykład, elementy HTML, do których przypisana jest wartość wyświetlania „none” nie pojawią się w drzewie, podczas gdy elementy z widocznością „hidden” będą tam widoczne. Są także takie elementy DOM, które odpowiadają wielu obiektom wizualnym – na przykład – element „select” ma trzy ramki – jedną w obszarze wyświetlania, jedną dla listy typu drop-down box i jedną dla przycisku.
  2. W drzewie DOM tekst reprezentowany jest przez jeden węzeł. Długość tekstu jest dla drzewa DOM nieistotna, ale ważna z punktu widzenia drzewa renderowania – jeśli szerokość tekstu jest większa od jednej linii, tekst będzie podzielony w wiele linii, które będą dodane jako kolejne węzły (omówimy to w dalszej części).

Celem drzewa renderowania jest uporządkowanie treści we właściwej kolejności. To drzewo zawiera informację na temat elementów wizualnych na stronie. W dalszej części artykułu, by uprościć, będę odnosić się do elementów drzewa renderowania jako „ramek”.

Drzewo renderowania jest kompleksem prostokątnych elementów – ramek – które powinny być wyświetlone na ekranie. Kiedy drzewo renderowania jest konstruowane, przechodzi proces tworzenia layoutu. Na tym etapie każda ramka dostaje szczegółowe współrzędne, w którym miejscu ekranu powinna się pojawić oraz atrybuty geometryczne takie jak szerokość i wysokość. Następnym etapem jest malowanie drzewa renderowania. Użytkownik widzi je jako wynik końcowy. Silniki renderowania w różnych przeglądarkach są zaprojektowane w różny sposób, ale działają podobnie.

Przyjrzyjmy się dwóm silnikom przeglądarek: WebKit i Gecko.

Zacznijmy od WebKit. Silnik renderujący otrzymuje HTML i style. Drzewo DOM jest konstruowane jako wynik analizy HTML. Wynikiem analizy CSS są reguły stylu. Kolejny etap – połączenie (atachment) – jest bardzo istotny. Drzewo renderowania jest konstruowane zgodnie ze stylizowaniem informacji razem z wizualnymi instrukcjami w HTML. Potem przechodzi proces layoutu. Na koniec malowane jest drzewo.

Jak możemy zauważyć, WebKit i Gecko używają odrobinę innej terminologii, ale sam sposób pracy jest właściwie taki sam. HTML i CSS są również analizowane, w rezultacie czego powstaje drzewo DOM, które określane jest mianem „Content Model”. Style są analizowane; tworzone jest drzewo stylu. Etap połączenia jest tu nazywany kontruktorem ramki (Frame Constructor), ale tak naprawdę oznacza to samo. W wyniku połączenia, tworzone jest drzewo renderowania (tu nazywane jest drzewem ramki – „Frame Tree”). Layout to reflow. Proces malowania nie zmienia swojej nazwy.

Dla uproszczenia:

  • Attachment = Frame Constructor
  • Drzewo renderowania = Frame Tree
  • Layout = Reflow

Przykład:

Mamy tu kilka tagów:

 

Silnik renderowania konstruuje drzewo DOM. W tym przypadku odbywa się to następująco. Jest tu element root (który jest obecny zawsze), który nazywany jest DocumentElement. Odpowiada on tagowi html. Drzewo zawiera wszystkie tagi. Zauważcie proszę, że tekst jest tu ujęty jako [text node]. A drzewo DOM nie wymaga żadnych dodatkowych informacji na temat tekstu. Drzewo renderowania jest tworzone na podstawie drzewa DOM.

Przykład:

Drzewo renderowania również posiada element root (RenderView), ale można łatwo zauważyć różnice pomiędzy drzewem DOM a drzewem renderowania. Nie ma tu tagu head, co znaczy, że nie jest on wyświetlany na ekranie. 

Tekst w drzewie renderowania został podzielony na dwie linie, które pojawiają się jako elementy: line 1 i line 2. Jak wspomniałem wcześniej, odnoszę się do elementów drzewa renderowania jako „ramek”. By to zilustrować, narysowałem je na obrazku.

Przykład:

Każda skrzynka ma „rodzica” oprócz elementu root.

Jednostka silnika renderującego jest również zaangażowana w przetwarzanie skryptów.

Kolejność przetwarzania skryptów i arkuszy stylu

Ważne, by zrozumieć kolejność przetwarzania skryptów. Spójrzmy na następujący przykład, w którym próbowałem pokazać wszystkie możliwe sposoby połączenia skryptów i stylów.

Script 1. Pierwsza rzecz, którą powinno się wiedzieć to to, że skrypty sa analizowane i wykonywane natychmiast, wtedy kiedy analizator dociera do tagu script. To oznacza, że jak tylko przeglądarka napotyka script 1., nie wie, jaka operację wykonać jako następną. Właśnie dlatego, analiza pozostałych części dokumentu zatrzymuje się az do momentu wykonania skryptu.

Podczas wykonywania skryptów, przegladarka kontunnuje spekulatywną analizę. Co to oznacza? Kolejny wątek analizuje resztę dokumentu i sprawdza jakie inne zasoby powinny być załadowane z sieci i robi to. W ten sposób zasoby mogą zostać załadowane w ramach równoległych połączeń, w momencie kiedy wykonywany jest script 1. W ten sposób zwiększona jest prędkość przetwarzania.

Biorąc pod uwagę powyższe, script 3 nie zostanie wykonany do momentu, w którym script 1 będzie przeanalizowany i wykonany. Istnieje szansa, że script 3 zostanie załadowany w pełni w momencie w którym script 1 zostanie wykonany. Skrypty mogą zostać umieszczone w tagi head i body. Różnica jest taka, że cały dokument jest analizowany w trakcie pracy z pierwszym skryptem – kiedy analiza jest zakończona i rozpoczyna się analiza skryptu drugiego, realne jest, że analiza całego dokumentu może być na tym etapie zakończona.

Skrypt może poziadać elementy takie jak „defer” i „async”. Są podobne, ale mają pewne różnice:

Możesz dodać atrybut „defer” do skryptu. W tym przypadku analiza dokumentu nie zatrzymuje się w momencie, kiedy skrypt jest wykonywany. W tym przypadku script 4 zostanie wykonany w momencie, kiedy przenalizowany zostanie cały dokument HTML i stworzone zostanie drzewo DOM.

Atrybut „async” oznacza, że analiza dokumentu nie zatrzymuje się w momencie wykonywania skryptu, ale jest analizowany i wykonywany przez inny wątek – asynchronicznie. W związku z tym, kolejność połączeń nie jest respektowana.

Atrybut „defer” powoduje również, że script 4 jest wykonywany po script 1. W przypadku użycia „async”, nie wiemy, kiedy będzie wykonany i która część dokumentu będzie w tym momencie analizowana.

Arkusze stylu, natomiast, mają inny model – nie mają wpływu na dokument. Skoro arkusze stylu nie zmieniają drzewa DOM, nie dodając tagów i węzłów, nie ma powodu, by analiza została zatrzymana.

Istnieje jednak pewien problem. Skrypty mogą prosić o informacje dotyczące stylu w momencie analizy dokumentu. Jeśli styl nie zostanie załadowany i zanalizowany na tym etapie, skrypt otrzymuje błędne informacje, a to powoduje problemy.

Przeglądarki próbują sobie z tym radzić. Na przykład – Firefox blokuje skrypty wtedy, kiedy istnieje arkusz stylów, który jest w danym momencie w trakcie ładowania i analizy. Chrome działa podobnie, ale jest lepiej zoptymalizowany. Blokuje skrypty tylko wtedy, kiedy próbują współpracować z niezaładowanymi arkuszami stylu.

Model pudełkowy (Box model)

Box = prostokątny obszar/blok = węzeł w drzewie renderowania

Model pudełkowy jest determinowany następującymi czynnikami:

  • Typ bloku (własność „display”)
  • Schemat pozycjonowania (pozycja i opływanie)
  • Wymiary bloku
  • Informacje zewnętrzne (rozmiar obrazka, rozmiar ekranu)

Przypuszczam, że wielu projektantów zna model pudełkowy, który opisuje kompozycję strony internetowej, layout drzewa renderowania i nie powienienem się na ten temat rozpisywać.

Istnieją pewne czynniki istotne dla kompozycji stron internetowych

Własność CSS „display”. Są dwa główne typy – inline oraz block. Inne, takie jak „inline-block table” pojawią się później. Różnica jest taka, że „display: block” oznacza, że szerokość prostokąta będzie przeliczona w zależności od szerokości „rodzica”. Własność „display: inline” oznacza, że szerokość prostokąta będzie w zależności od jego zawartości. Jeśli element zawiera dwa słowa, szerokość prostokąta będzie równa szerokości służącej wyświetleniu tych słów. Elementy „inline” są ustawione sekwencyjnie.

Schemat pozycjonowania jest regulowany za pomocą własności „position” i „float”. W pozycjonowaniu statycznym, pozycja nie jest definiowana i używane jest pozycjonowanie domyślne. Istnieje również pozycjonowanie relatywne i absolutne. Relatywne powoduje, że obiekt jest na początku umieszczony w położeniu spoczynkowym, następnie może zostać przemieszczony w lewo, prawo, górę i dół, w zależności od użytych wartości.

Własności „absolute” i „fixed” wywołują pozycjonowanie absolutne, w ramach którego element jest „wyjęty” ze swojej oryginalnej pozycji i nie bierze udziału w standardowym biegu dokumentu. Inne elementy nie są brane pod uwagę. Współrzędne są tu obliczane w relacji do głównego elementu (root) albo rodzica, jeśli nie jest położony statycznie. Również własność „float” wpływa na pozycjonowanie. Wskazuje na to, że dany element bierze udział w normalnym biegu dokumentu, ale jest położony skrajnie – albo z lewej, albo z prawej strony. Wszystkie inne zawartości „opływają” ten element.

Podsumowując tę część artykułu chciałbym dodać, że główny schemat działania przelądarki to nieskończona pętla wspierająca poszczególne procesy – otrzymuje także zdarzenia typu „reflow” albo „repaint”. Te zdarzenia są przesyłane przez silnik renderujący, po otrzymaniu ich jest miejsce na odpowiednie akcje.

W przeglądarce Firefox, silnik renderujący działa jednowątkowo. Jest tylko jeden taki silnik dla całej przeglądarki. W przypadku Chrome wygląda to odrobinę inaczej – każda zakładka ma swój własny silnik renderujący.

Ważne jest, że moduł sieciowy operuje na oddzielnych wątkach równoległych, które nie są związane z silnikiem renderującym. Dlatego, komponent sieciowy może korzystać z zasobów bez względu na procesy zachodzące w silniku renderującym. Zazwyczaj taki komponent może pracować jednocześnie z wieloma połączeniami i uploadować wiele plików w tym samym czasie. Firefox, na przykład, ma 6 równoległych strumieni ładowania treści, skryptów, itd.

W kolejnej części artykułu opiszę zdarzenia „reflow” i „repaint” i wyjaśnię, jak pracować z nimi, by zwiększyć wydajnośc aplikacji.