На этой лекции мы заглянем под капот браузеров и посмотрим как они работают. Иметь представление о том, как работают браузеры важно по двум причинам. Во-первых, это позволяет учесть некоторые нюансы при разработке приложения, чтобы сделать его более эффективным. Во-вторых, это интересно и расширяет кругозор!
Когда-то давно между браузерами бушевала война, каждый по-своему реализовывал стандарты, некоторые добавляли свою функциональность. Все это было ужасом для простого мирного разработчика. Но с тех пор времена изменились, браузеры подружились и сплотились вокруг технологий. Придерживаются стандартов и продвигают новые крутые штуки в мир.
Историю развития браузеров, стандартов и технологий можно посмотреть здесь
На текущий момент в нашем распоряжении четыре основные группы браузеров. В группы они объединяются исходя из их основы, движка, на основе которого они построены.
- Internet Explorer, Microsoft Edge - два браузера от компании Microsoft. Microsoft Edge является заменой старичка IE. Тем не менее этот браузер все еще сильно распространен, поэтому его нельзя сбрасывать со счетов.
- Firefox от компании Mozilla.
- Safari от компании Apple. В данный момент браузер доступен только на Mac
- Google Chrome, Yandex Browser, Opera - тройка от соответствующих компаний, основанные на открытом проекте Chromium Это основные десктопные браузеры. В мобильных устройствах используются другие версии вышеперечисленных. На фоне всех мобильных браузеров ярко выделяется Opera Mini, которая работает через свой прокси-сервер. На этом сервере происходит переформатирование веб-страниц в собтсвенный формат, сжатие данных, что значительно ускоряет загрузку на мобильных устройствах. Иногда это приводит к неожиданным side-эффектами.
Основной функцией браузера является отображение. Отображение картинок, изображений, HTML-страниц, PDF-документов и так далее. Все это какие-то веб-ресурсы. И чтобы наши веб-ресурсы отображались в разных браузерах одинаково необходимо, чтобы они действовали по одинаковым правилам. Эти правила хранятся в спецификации W3C.
Рассмотрим из чего же состоит современный браузер:
Пользовательский интерфейс. Конечно, чтобы мы могли сообщить на какой сайт мы хотим посмотреть, нам нужен какой-то интерфейс. Он должен состоять из строки ввода, кнопок перехода по истории (вперед-назад), хранения избранных ресурсов. Современные браузеры предоставляют огромное количество инструментов для пользователя. И все это интерфейс для того, что решить одну простую задачу – посмотреть на картиночку или сайтик на просторах интернета.
Движок браузера (Browser Engine). Движок – модуль, занимающийся взаимодействием UI-интерфейса и модуля отображения.
Модуль отображения (Render engine). Вот она – главная составляющая браузера. Основной задачей, что понятно и из названия, этого модуля как раз является отображение веб-ресурса.
Чтобы решать связанные с отображением и получением ресурсов по сети у браузера есть ряд других компонент:
- Хранилище данных. В этом хранилище могут храниться любые данные, например, кеш или cookies. Браузер также предоставляет ряд модулей для работы с хранилищем, например, localStorage, IndexedDB, WebSQL.
- Сетевые компоненты. Позволяют делать сетевые запросы. Например, делать HTTP-запросы за картинками,
js
иcss
файлами. - Интерпретатор JavaScript. Модуль, который осуществляет парсинг и выполнение JavaScript в браузере.
- Backend UI. Модуль, взаимодействующий с операционной системой для отрисовки базовый контролов, таких как: селекты, кнопки, чекбоксы, радиобаттоны. Использует интерфейс конкретной ОС.
Дальше мы рассмотрим отдельные компоненты более подробно.
Рассмотрим очень поверхностно как браузер обрабатывает запрос пользователя, чтобы отобразить ему желаемую страницу.
Когда пользователь вводит в строку ввода адрес сайта. Браузеру проверяет не лежит ли у него эта страница в кеше, если она там, то отдает сразу пользователю.
Если же в кеше странице нет, браузеру приходится сделать за ней запрос. Но это не один запрос, процесс чуть сложнее. Сначала браузер делает DNS Lookup, чтобы получить ip адрес по названию сайта. Но прежде чем сервер и браузер начнут обмениваться данными, происходит процесс подтверждения стартового пакета.
Данный процесс называется three-way TCP Handshake.
- Браузер посылает SYN-пакет, содержащий случайно выбранный номер последовательности.
- Сервер инкрементирует номер, добавляет свой номер последовательности и отправляет в ответ пакет с флагами SYN ACK
- Браузер инкрементирует оба номера и заканчивает рукопожатие возвратив пакет с флагом ACK После этого браузер и сервер могут начинать обмениваться данными.
Браузер делает запрос на сервер, после чего сервер немного (или много) думает и начинает возвращать страницу, которую браузер обрабатывает и отображает пользователю.
Зачем кроме расширения кругозора нам вся эта информация? Она важна для понимания того, что до первого запроса на сервер, может пройти значительное время. Сначала время тратится на DNS Lookup. Затем целых два запроса на TCP Handshake. И только после этого запрос уходит на сервер.
Стоит упомянуть еще одну интересную вещь. Вместе с запрсом браузер посылает свой user-agent, это текстовое поле, отражающее с какого браузера был сделан запрос. Можно считать это поле идентификатором браузера. Однако, не стоит на него полагаться на все сто, так почти все современные браузеры умеют его менять, чтобы притворяться другим.
Не менее интересно значение этого поля. Вот, например, как выглядит user-agent моего основного браузера: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 YaBrowser/15.10.2454.3314 Safari/537.36
. Из него можно понять, что это Яндекс браузер версии 15.10.2454.3314
, а также же можно понять какая операционна система стоит на моем ноутбуке.
Тем неменее бросается в глаза названия других браузеров (Mozilla
, Safari
, Chrome
). К сожалению, на подробный рассказ в этой лекции у нас нет времени, но можно прочитать старенькую, но захватывающую историю о появлении user-agent.
Теперь мы рассмотрим как собственно браузер отображает HTML-страницы. Сначала рассмотрим общую схему, чтобы получить всю картину, а затем рассмотрим отдельные части более подробно.
После того, как браузер получил HTML, запускается процесс отображения.
Сначала происходит парсинг HTML и создание DOM-дерева, которое иногда называют Content tree
.
Далее парисится CSS и преобразование его в CCSOM. CSSOM, как мы увидим позже, тоже является деревом.
На основе CSSOM и DOM создается Render tree или дерево отображения. В Firefox это дерево называется Frame Tree. Элементы Render tree – это прямоугольники с визуальными элементами (цвета). В дерево включаются только видимые элементы.
На основе Render tree запускается процесс компоновки. В Firefox этот процесс называется reflow, а в Chrome – layout. На этом этапе каждому прямоугольнику выставляются координаты для отображения на экране
Далее, наконец, наступает процесс отрисовки элементов Redner tree на экране. Тут как раз задействуется Backend UI, для рисования системных контролов. Процесс отрисовки называется Painting.
В разных браузера используются разные модули отображения. Но основных всего четыре, по одному на каждую из выделенной нами группу.
- В Internet Explorer используется Trident В Microsoft Edge используется EdgeHTML, который является форком Trident
- В Firefox – Gecko
- В Safari – WebKit
- В браузерах, основнанных на Chromium – Blink, который является форком от WebKit.
Для примера, взглянем на процесс отображения в WebKit и Gecko. Процессы в этих движках очень схожие, но используются разные термины.
Объединение DOM и визуальных атрибутов (из CSSOM) в WebKit называется Attachment. В остальном вся схема полностью ложится на описаный выше нами процесс.
Как видно из картинки Gecko также соответствует описанному процессу.
Далее мы рассмотрим немного подробнее каждый из этапов процесса отображения.
В процессе получения HTML от сервера, браузер преобразовывает пришедшие байты в символы, основываясь на кодировке. Далее на основании стандарта W3C браузер выделяет из текста теги. Для каждого тега, есть собственный набор правил. Для каждого тега создается объект с соответствующими свойствами. С помощью этих свойств конструируется DOM дерево. Иерархия выстраивается на основании вложенности тегов.
Для того, чтобы правильно распарсить HTML, браузер ориентируется на его Doctype. Правила же разбора HTML описываются с помощью схемы DTD. Схема DTD описывает теги и их доступные атрибуты и значения.
Рассмотрим преобразование в DOM на примере:
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Html parsing example</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
</html>
Из этого примереа получается DOM:
Более подробнее про преобразование можно посмотреть на это видео
Пр парсинге браузер нам очень много всего прощает. Мы никогда не увидем ошибку, даже если напишем совсем кривой HTML. Даже в этом случае постарается его исправить.
Рассмотрим как же браузеры исправляют некоторые ошибки.
Неправильное использование тегов
</br>
Браузеры исправят такой тег и он будет обработан как <br>
.
Неправильное вкладывание тегов друг в друга Следующий пример про неправильное использование иерархии тегов. Попытаемся вложить ссылку в ссылку:
<a href="#">
<a href="#">
inner link
</a>
outer link
</a>
Когда браузер находит вложенную ссылку, он считает, что предыдущая уже закончилась и закрывает ее:
<a href="#"></a>
<a href="#">
inner link
</a>
outer link
</a>
А последний тег просто игнорирует, получается вот такой результат:
<a href="#"></a>
<a href="#">
inner link
</a>
outer link
Использование вложенных форм Рассмотрим пример:
<form action="#a1">
outer form
<form action="#a2">
inner form
</form>
</form>
Браузеры игнорируют все внутренние формы, то есть просто берут и выбрасывают их из разметки:
<form action="#a1">
outer form
inner form
</form>
Несмотря на та, что синтаксический анализ браузера имеет "щадящий" характер, не стоит на него полагаться. Правила обработки ошибок не описаны в спецификации и отданы на откуп браузеров. Поэтому нет никакой гарантии, что одна и та же ошибка будет одинаково обработана в разных браузерах. Пишите HTML правильно!
Для проверки корректности HTML можно воспользоваться средствами IDE или онлайн валидатором
В целом схема разбора аналогичная парсингу HTML. Браузер получает байты, преобразует их в текст. Далее текст прогоняется через синтаксический анализатор, который выделяет элементы и соотвествующие им правила. Элементы и правила заворачиваются в объекты StyleSheet. Для описания синтаксических правил используется формат BNF (Форма Бэкуса – Наура), из можно найти так же в спецификации.
На этом шаге учитывается каскад, то есть на каком уровне описаны стили, а также специфичность.
Стили выстраиваются в иерархию от более общего к более специфичному.
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
В итоге получаем дерево CSSOM:
Аналогично HTML, неправильно написанный CSS не выкинет ошибку. Но отличие заключается в том, что для парсинга используется четко описанный синтаксис, те части, которые анализатор не понимает или которые не подходят под правила – игнорируются. В большинстве случаев либо "съедаются" либо игнорируются некоторые правила.
.foo {
width: 10em
height: 10em;
background: red;
}
.bar {
width: 10bem;
height: 10em;
background: red;
.baz {
width: 10em;
height: 10em;
background: red;
}
В данном примере есть несколько проблем.
Во-первых, пропущена точка с запятой width: 10em
. Анализатор в этом случае "съест" оба правила – width
и height
.
Вторая проблема – в правиле width: 10bem;
указана неизвестная единица измерения. Анализатор просто проигнорирует это правило.
Третья, куда более опасная, проблема – потерянная закрывающая скобочка к селектору bar
. В этом случае анализатор "съест" все что идет после последней валидной строчки (background: red;
) и до закрывающей скобочки.
В результате от наших CSS стилей анализатор "откусил" большую часть, результат очень плачевный:
.foo {
background: red;
}
.bar {
height: 10em;
background: red;
}
Чтобы не попасть в такую ситуацию, даю лайфхак – пишите CSS правильно!
Из-за того, что стили участвуют в построении Render tree, следует несколько правил, которых придерживается браузер:
- Стили начинают скачиваться сразу при обнаружении
- Стили блокируют общий рендеринг страницы и построение Render tree в частности
Браузер также ориентируется на media-запросы, чтобы понять нужно загружать те или иные стили.
На данном этапе у нас есть два дерева – DOM и CSSOM. Результатом их скрещивания будет новое дерево – Render tree.
Render tree строится на основании DOM, поэтому эти деревья могут быть очень похожи. Однако, это не обязательно так. В Render tree попадают только видимые элементы, а значит некоторые элементы из DOM, например, имеющие display: none
, будут отсутствовать в Render tree. Обратите внимание, что элемент со свойством visibility: hidden
хоть и является невидимым, но тем не менее он будет включен в Render tree, так как влияет на отображение. Точно также в Render tree могут присутствовать элементы, которых нет в DOM. Они могут быть добавлены на основании, например, необходимости в псевдоэлементах :before
и :after
.
Общую схему можно представить примерно так:
- На основании DOM, начинается построение Render tree
- Для каждого элемента добавляются соответствующие стили из CSSOM
- Исключаются невидимые элементы
- Добавляются новые при необходимости
- Результат – дерево, с элементами которого являются прямоугольники с визуальными стилями
После того, как браузер получил Render tree начинается процесс компоновки, или reflow, или layout. На этом шаге браузер для каждого узла полученного узла высчитывает размеры и положение на экране.
Используется поточная модель компоновки. Компоновка осуществляется слева направо, сверху вниз. Элементы, встречающиеся позже, не влияют на геометрию предыдущих. Исключением является таблица. Система координат рассчитывается относительно корневого фрейма.
Компоновка проходит в несколько циклов. Начинается с корневого элемента (<html>
) и дальше вниз по иерархии. Каждый элемент сначала обсчитывает своих потомков, затем себя.
Браузер использует два вида компоновки: глобальную и инкрементальную. Глобальная – это компоновка всего Render tree. Например, глобальная компоновка может быть вызвана изменением размера окна или глобальным изменением окна, это трудоемкий процесс.
Для того, чтобы не вызывать при каждом чихе глобальную компоновку браузер использует систему грязных битов. Изменный элемент и все его потомки помечаются как "грязные", другими словами, требующие перекомпоновки. Для таких элементов запускается инкрементальная компоновка. Такой процесс обычно выполняется асинхронно при обнаружении "грязных" элементов.
"Грязные" элементы могут появляться при частичном изменении DOM или соответствующих стилей. Вот несколько примеров:
- Добавление или удаление ноды в DOM
- Смена класса у DOM ноды
- Смена состояния: фокус, ховер, и тп.
Процесс компоновки выглядит примерно так:
- Элемент определяет собственную ширину
- Элемент обрабатывает дочерние элементы
- На основании суммарной высоты дочерних элементов, полей и отступов, вычисляется высота
На этом видео можно увидеть процесс компоновки в Firefox.
Когда браузер закончил вычислять размеры и координаты элементов Render tree, можно их отображать на экране. Этот процесс называется отрисовкой или painting. Также можно встретить названия: визуализация и растеризация.
На этом этапе браузер отрисовывает каждый элемент из дерева Render tree. У каждого элемента вызывается метод paint
и результат рисуется на экране.
Процесс отрисовки также делится на два типа: глобальный и инкрементальный. Логика точно такая же как и с компоновкой: глобальная отрисовка вызывается для всего дерева, а инкрементальная только для конкретного элемента и его потомков.
При отрисовке учитывается стековый каскад (z-index
), чтобы перекрывать одни элементы другими.
Используется определенный порядок применения свойств и отрисовки дочерних элементов
- Цвет фона
- Фоновое изображение
- Граница
- Тень
- Дочерние элементы
- Outline Поэтому когда мы используем одновременная цвет фона и фоновую картинку, картинка оказывается поверх цвета.
Порядок отрисовки описан в спецификации