Важливі аспекти роботи браузера для розробників


Автор: Антон Реймер

Стаття заснована на вебінарі, який я проводив деякий час назад. Розрахована вона, в першу чергу на тих, хто не знає, як працюють браузери, або тих, у кого є прогалини в знаннях. Ймовірно, тут буде багато очевидного для тих хто не перший день у веб-розробці. Статтю я вирішив розділити на дві частини. У першій розглянемо загальні принципи роботи браузера. У другій частині я акцентую увагу на деяких важливих моментах: reflow і repaint, event loop.

Що таке браузер?


Браузер — програма, що працює в операційній системі. Більшість браузерів написано на мові C++. Основне призначення браузера — відтворювати контент з веб-ресурсів. В якості веб-ресурсу в більшості випадків виступає html-сторінка. Це також може бути pdf, png, jpeg, xml-файли і інші типи. Серед величезної кількості браузерів можна виділити найпопулярніші: Chrome, Safari, Firefox, Opera і Internet Explorer. Ми розглянемо браузери з відкритим вихідним кодом: Chrome, Firefox, Safari.

З чого складається і як працює браузер?


На схемі зображені модулі браузера, кожен виконує власну функцію. Почнемо з користувальницького інтерфейсу.

Користувальницький інтерфейс — те, що бачить перед собою користувач, тобто адресна рядок, елементи навігації, власне меню і т. д. Незважаючи на те що користувальницькі інтерфейси дуже схожі один на одного, ніякого стандарту, який описував би, не існує. Так історично склалося, що браузери поступово переймали інтерфейс один у одного і ставали все більш схожими.

Механізм браузера відповідає за взаємодію користувача інтерфейсу і модуля відображення, а також за збереження даних у пам'яті.

Модуль відображення. Цей модуль — найважливіший для розробників. Робота розробника, в першу чергу, відбувається саме з ним, а як можна зрозуміти з назви — він відповідає за відображення інформації на екрані.

Коли ми говоримо про браузерних движках, таких як Webkit або Gecko (перший знаходиться «під капотом» у Safari і до 2013 року був у Chrome, другий в Firefox), у першу чергу маємо на увазі модуль відображення. Далі ми докладно розглянемо модуль відображення і більш детально розберемо, як він працює.

Наступний модуль — мережеві компоненти. Він відповідає на запити з мережі, бере дані з зовнішніх ресурсів і взаємодіє з модулем відображення.

Модуль JS Interpreter відповідає за інтерпретацію скрипта, і його виконання. Існує кілька JS-движків. Найвідоміші це V8 і JavaScriptCore. Важливо не плутати движок браузера і JS-движок, який працює в модулі JS Interpreter.

Наступний модуль — виконавча частина інтерфейсу користувача (UI backend). Вона відповідає за малювання всього на екрані і роботу користувальницького інтерфейсу.

Останній модуль — сховище даних. Браузеру потрібно десь зберігати дані, які зазвичай для цього використовується оперативна пам'ять. Які дані потрібно зберігати? Наприклад, кеш, власні налаштування. Також до сховища даних можна віднести indexedDB, який з'явився в стандарті html5 — власні бази даних браузера.

Модуль відображення


Модуль відображення отримує дані від мережевого модуля. Дані надходять пакетами по 8 Кб. Що важливо — модуль відображення не чекає, поки прийдуть всі дані, він починає обробляти і виводити їх на екран по мірі надходження. У випадку з html-сторінками, він починає їх аналізувати, відбувається парсинг html (це окрема велика тема, я на ній зупинятися не буду). Головне, що потрібно розуміти: в результаті парсинга у нас з'являється DOM-дерево. Також по закінченні парсинга спрацьовує подія load, яке можна обробляти в скрипті. Це означає, що документ готовий і скрипт може з ним працювати.

DOM-дерево — document object model. За великим рахунком, «інтерфейс», який надає браузер JS-движку для роботи з тим чи іншим html-документом. На основі DOM-дерева відбувається конструювання дерева відображення (render tree). Дерево відображення — теж важлива частина модуля відображення. За великим рахунком, два дерева — DOM-дерево і дерево відображення найбільш важливі елементи для розробника. Дерево відображення в чому повторює структуру DOM-дерева (далі буде приклад, де це буде представлено наочніше), але має деякі відмінності:

  1. Дерево відображення не містить прихованих елементів. Якщо у нас є html-елемент, у якого прописаний
    display:none
    , в дереві відображення він присутнім не буде. При цьому, якщо
    visibility:hidden
    , то в дереві відображення він буде. Деякі DOM-вузли, які в DOM-дереві представлені як єдиний вузол в дереві відображення можуть бути представлені у вигляді кількох. Яскравий приклад — складовою тег select. Якщо в DOM-дереві це один вузол в дереві відображення, він перетворюється в мінімум три вузли. Перший вузол відповідає за відображення обраного елементу. Другий — за список, що випадає з можливими пунктами. І, нарешті, третій блок відповідає за стрілочку.
  2. Текст в DOM-дереві представлений як проста node. DOM-дерева немає ніякого діла до того, що там написано, скільки рядків цей текст займає. В той час, як для дерева відображення — це важливо, і текст трансформується в кілька вузлів, в залежності від того скільки рядків він займає. Це наочніше розглянемо трохи пізніше.


Дерево відображення служить для того, щоб браузер розумів, що виводити на екран. Воно містить інформацію про те, з яких блоків складається сторінка. Далі в тексті для простоти я буду називати складові частини дерева відображення прямокутниками, щоб не плутати з html блоками.

Дерево відображення — сукупність прямокутників, яка повинна бути виведена на екрані. Після того як дерево відображення сконструйоване, слід етап компоновки. На цьому етапі всім прямокутникам присвоюються розміри і координати. Кожен прямокутник отримує свої ширину і висоту, координати у вікні браузера. Після компонування відбувається побудова дерева відображення. Користувач бачить вже кінцевий результат. Модуль відображення у кожному браузері влаштований по-своєму, але схема роботи схожа.

Пропоную розглянути два браузерних движка: Webkit і Gecko.


Webkit. Модуль відображення отримує html і стилі. В результаті парсинга html виникає DOM-дерево. В результаті парсинга CSS виникає дерево правил таблиць стилів (Style Rules). Далі йде важливий етап, який називається Attachment, можна перевести, як «поєднання». На цьому етапі CSS-стилі накладаються на DOM-дерево, у результаті чого з'являється Render Tree. Після чого відбувається компонування дерева. Називається вона тут Layout. І в завершенні відбувається відтворення (Painting).


Якщо подивитися на Gecko, можна помітити, що схеми дуже схожі. Головні відмінності — в термінології. Тут теж парсятся HTML, CSS. В результаті чого створюється DOM-дерево, яке тут називається Content Model. Парсятся стилі, утворюється дерево стилів. Етап Attachment тут називається Frame Constructor, але, по суті, це теж саме. В результаті поєднання утворюється дерево відображення, тут воно називається Frame Tree. Компонування тут називається Reflow. А побудова називається Painting, так само, як і Webkit.

Для простоти приравняем деякі терміни:
  • Attachment = Frame constructor = Суміщення
  • Render Tree = Frame Tree = Дерево відображення
  • Layout= Reflow = Компонування


Приклад




Тут у нас є теги:
<head> <p> є <div style =" display: none"> і ще один <div><img src>"..."/></div>




Модуль відображення будує DOM-дерево. В даному випадку воно буде виглядати наступним чином. Є кореневий елемент (він завжди присутній) називається він
documentElement
відповідає тегу
html
. У цьому дереві присутні всі теги. І зауважимо, що текст представлений як
[text node]
. І DOM-дерева більше нічого про тексті знати не потрібно. На основі цього DOM-дерева будується Render Tree.

Приклад




Дерево відображення. У нього також є кореневий елемент (RenderView), але вже можна побачити відмінності між DOM-деревом і деревом відображення. По-перше, немає тега
head
, оскільки він не відображається на екрані. Немає
<div style =" display: none">
, є тільки

<div><img src>"..."/></div>

Текст в дереві відображення розділився на два рядки і являє собою два елемента: line 1 і line2. Як я писав вище, вузли дерева відображення ми будемо називати прямокутниками. Для наочності я так і відобразив їх на ілюстрації.

Приклад




Кожен прямокутник має свого «батька», крім кореневого елемента root.

Модуль відображення також займається обробкою сценаріїв.

Порядок обробки скриптів і таблиць стилів


Важливо розуміти порядок, у якому відбувається обробка скриптів. Розглянемо наступний приклад, де я спробував продемонструвати всі можливі способи підключення скриптів і стилів.


<html>
<head>
<script src="script1.js"></script>
<script src="http://site.comscript3.js"></script>

<script defer src="script4.js"></script>
<script async srcs="script5.js"></script>

<link rel="style" src="style.css"></link>
</head>
<body>

...

<script src="script2.js"></script>

</body>
</htlm>


Скрипт 1. Перше, що потрібно знати про скрипти, — коли при парсингу
html
аналізатор зустрічає скрипт, він зупиняє подальший парсинг документа. Тобто, як тільки аналізатор дійшов до скрипта 1, браузеру нічого невідомо про те, що буде далі. І поки скрипт 1 не виконається, подальший аналіз документа відбуватися не буде.

Але при цьому браузер продовжує виконувати орієнтовний синтаксичний аналіз. Що це означає? Браузер все одно дивиться, що слідує за скриптом. Якщо знаходяться посилання на зовнішні ресурси, які потрібно завантажити і завантажити, він довантажити ці дані, поки виконується скрипт 1. Зроблено це для оптимізації.

При цьому скрипт 3 все одно не буде виконуватися, поки не виконається скрипт 1. До моменту, коли скрипт 1 вже виконається, скрипт 3 вже може бути повністю завантажений. Скрипти можна вставляти в теги
head
та
body
. Різниця в тому, що в скрипті 2, на відміну від скрипта 1, практично весь документ вже буде проаналізовано.
У скрипта можуть бути атрибути, такі як
deffer
та
async
. Вони схожі, але у них є відмінності:

  • Атрибут
    deffer
    повідомляє браузеру, щоб той не чекав закінчення виконання скрипта, а продовжував парсинг html-сторінки. При цьому скрипт 4 виконається тільки після того, як весь html-документ буде проаналізовано та побудовано DOM-дерево.
  • Атрибут
    async
    теж говорить браузеру, що подальший html-документ може бути проаналізований, поки скрипт виконується. Але при цьому він виконується в паралельному потоці, тобто він насправді виконується асинхронно. Це означає, що він може бути виконаний раніше, ніж скрипт1. Якщо в скрипті 1 у нас 1000 рядків, а в скрипті 5 – 10, скрипт 5 ймовірно буде виконаний раніше. Тобто порядок підключення в цьому випадку не дотримується.


У випадку з
defer
скрипт 4 завжди виконується після скрипта 1. З атрибутом
async
невідомо, коли він буде виконаний і яка частина документа вже буде проаналізована до цього моменту.

Стилі, на відміну від скриптів, ніяк не можуть вплинути на документ. Якщо скрипти можуть додати додаткові вузли або теги стилі цього зробити не можуть. Тому ніякої потреби для браузера блокувати подальший аналіз документа немає.

При цьому є невеликий нюанс. Наприклад, скрипт 1 може працювати з тими чи іншими стилями, і може знадобитися доступ до них. Тобто якщо ми хочемо поміняти (або дізнатися якісь стилі, але при виконанні скрипта 1 вони ще не завантажено — може статися помилка.

Браузери намагаються врахувати цей нюанс. Firefox, наприклад, якщо знаходить якісь не подгруженные стилі в процесі орієнтовного синтаксичного аналізу, блокує виконання скрипта, довантажує стилі, після чого завершує виконання скрипта. Chrome діє аналогічним чином, але трохи більше оптимізовано. Він зупиняє скрипт, тільки якщо розуміє, що в цьому скрипті відбувається робота з не подгруженными стилями.

Компонування вікон




Вікно = Прямокутник = Вузол дерева відображення

Спосіб компонування вікна визначається наступними факторами:
  • Тип вікна (властивість display).
  • Схема позиціонування (властивості position і float).
  • Розміри вікна.
  • Зовнішня інформація (розмір зображення, розмір екрану).


Компонування вікон — це етап компонування дерева відображення. Я думаю багатьом верстальникам знайома ця схема, вона називається «Box model». Я не буду докладно на ній зупинятися.

При компонуванні вікон враховуються наступне фактори:

CSS властивість display. Два основних типи — inline і block. Інші, такі як inline-block table та інші, з'явилися вже пізніше. Відмінність в тому, що display:block, вказує, що ширина прямокутника буде обчислюватися в залежності від ширини «батька». А display:inline вказує, що ширина прямокутника буде обчислюватися в залежності від його вмісту. Якщо в елементі два слова, ширина прямокутника буде дорівнювати ширині, необхідної для виведення цих слів. Inline-елементи вишиковуються один за одним. А блокові елементи — один під одним.

Наступне, що впливає на компонування елемента, властивості position і float. Position за замовчуванням static, при цьому прямокутник йде в стандартному потоці компонування. Також є position:relative і position:absolute. Position:relative вказує, що прямокутника виділяється місце в стандартному потоці компонування. При цьому позиція елемента може бути зрушена щодо цього місця: вліво, вправо, вгору, вниз за допомогою відповідної властивості.

Абсолютне позиціонування, до якого відноситься position:absolute і position:fixed, вказує, що елемент виходить за межі свого прямокутника із загального потоку компонування. Інші прямокутники його не враховують. Він також не враховує сусідні елементи. Координати його обчислюються щодо кореневого елементу сторінки, або щодо предка, у якого position не static. Розміри ж обчислюються теж щодо батьків. Також на позиціонування впливає властивість float. Воно вказує, що наш прямокутник йде в стандартному потоці, але при цьому займає або крайню ліву, або крайню праву позицію. При цьому всі інші прямокутники «обтікають» цей елемент.

На закінчення цієї частини варто сказати що, основний потік браузера являє собою нескінченний цикл, підтримує робочі процеси. Він чекає відправки подій, таких як reflow і repaint. Ці події йому приходять від модуля відображення. Отримавши їх, він виконує відповідні дії.

У Firefox модуль відображення працює в одному потоці. Він єдиний на весь браузер. У Chrome все трохи інакше: модуль відображення і потік виконання у кожної свої вкладки.

Важливо, що мережевий модуль працює в окремих паралельних потоках, які не пов'язані з модулем відображення. Отже, мережевий компонент може використовувати ресурси незалежно від того, що відбувається в модулі відображення. Зазвичай у такого компонента є можливість працювати одночасно з декількома з'єднаннями і завантажувати відразу декілька файлів. У Firefox, наприклад, може бути шість паралельних потоків, за допомогою яких можна завантажувати контент, скрипти і т. д.

У наступній частині ми детально розглянемо події reflow і repaint і спробуємо зрозуміти як грамотна робота з ними може підвищити швидкість роботи програми.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.