Как работает JS: движки рендеринга веб-страниц и советы по оптимизации их производительности

в 8:32, , рубрики: javascript, Блог компании RUVDS.com, Клиентская оптимизация, разработка, Разработка веб-сайтов, рендеринг

Сегодня, в переводе одиннадцатой части серии материалов, посвящённых JavaScript, мы поговорим о подсистемах браузера, ответственных за рендеринг веб-страниц. Они играют ключевую роль в деле преобразования описаний документов, выполненных с помощью HTML и CSS, в то, что мы видим на экране.

image

Автор материала говорит, что в компании SessionStack приходится уделять рендерингу огромное внимание. В этой статье он поделится советами, касающимися оптимизации веб-страниц с учётом особенностей их визуализации.

Обзор

Создавая веб-приложения, мы не пишем изолированный JS-код, который занимается исключительно какими-то собственными «внутренними» делами. Этот код выполняется в окружении, предоставляемом ему браузером, взаимодействует с ним. Понимание устройства этого окружения, того, как оно работает, из каких частей состоит, позволяет разработчику создавать более качественные программы, даёт ему возможность предусмотреть возникновение возможных проблем с приложением, которое вышло в свет.

На рисунке ниже показаны основные компоненты браузера. Давайте поговорим о том, какую роль они играют в процессе обработки веб-страниц.

Как работает JS: движки рендеринга веб-страниц и советы по оптимизации их производительности - 2

Основные компоненты браузера

  • Пользовательский интерфейс (User Interface). Этот компонент браузера включает в себя адресную строку, кнопки «Вперёд» и «Назад», команды для работы с закладками, и так далее. В целом, это всё то, что выводит на экран браузер — за исключением той области его окна, где находится отображаемая им веб-страница.
  • Движок браузера (Browser Engine). Он занимается поддержкой взаимодействия между пользовательским интерфейсом и движком рендеринга.
  • Движок рендеринга (Rendering Engine). Эта подсистема отвечает за показ веб-страницы. Движок рендеринга обрабатывает HTML и CSS и выводит то, что у него получилось, на экран.
  • Сетевая подсистема (Networking). Эта подсистема ответственна за сетевое взаимодействие браузера с внешним миром, в частности, например, её средствами выполняются XHR-запросы. Она поддерживает платформенно-независимый интерфейс, за которым скрываются конкретные реализации различных сетевых механизмов, специфичные для различных платформ. Здесь можно почитать подробности об этой подсистеме.
  • Подсистема поддержки пользовательского интерфейса (UI Backend). Эта подсистема отвечает за вывод базовых компонентов интерфейса, таких, как окна и элементы управления, вроде чекбоксов. Здесь браузеру предоставляется универсальный интерфейс, не зависящий от платформы, на которой он работает, а в основе этой подсистемы лежат возможности формирования элементов пользовательского интерфейса, предоставляемые конкретной операционной системой.
  • JavaScript-движок (JavaScript Engine). Мы разбирали JS-движок в одном из предыдущих материалов этой серии. Именно здесь осуществляется выполнение JS-кода.
  • Подсистема постоянного хранения данных (Data Persistence). Если приложению нужны возможности локального хранения данных, оно может пользоваться различными механизмами, предоставляемыми этой подсистемой. Среди них, например, такие API, как localStorage, IndexedDB, WebSQL и FileSystem.

В этом материале мы сосредоточимся на движке рендеринга. Именно эта подсистема браузера занимается разбором и визуализацией HTML и CSS. А это — именно те технологии, с которыми постоянно взаимодействует код веб-приложений, написанный на JavaScript.

О различных движках рендеринга

Главная задача движка рендеринга заключается в том, чтобы вывести запрошенную страницу в окне браузера. Движок может выводить HTML-документы, XML-документы, изображения. При использовании дополнительных плагинов движок может визуализировать и материалы других типов, например — PDF-документы.

Мы знаем, что существуют различные JS-движки, которые используют различные браузеры. То же самое справедливо и для движков рендеринга. Вот несколько популярных движков:

  • Gecko — используется в браузере Firefox.
  • WebKit — применяется в браузере Safari.
  • Blink — интегрирован в браузеры Chrome и Opera (с 15-й версии).

Процесс рендеринга веб-страницы

Движок рендеринга получает содержимое запрошенного документа от сетевого уровня браузера. Процесс рендеринга выглядит так, как показано на рисунке ниже.

Как работает JS: движки рендеринга веб-страниц и советы по оптимизации их производительности - 3

Процесс рендеринга веб-страницы

Вот основные этапы этого процесса:

  • Обработка HTML для создания дерева DOM.
  • Создание дерева рендеринга.
  • Расчёт параметров расположения элементов дерева рендеринга на экране, формирование макета страницы.
  • Визуализация (отрисовка) дерева рендеринга.

Рассмотрим эти и другие шаги, выполняемые при визуализации веб-страниц, подробнее.

Создание дерева DOM

Первый этап работы движка рендеринга заключается в разборе HTML-документа и преобразовании того, что у него получилось, в узлы DOM, размещённые в дереве DOM. При этом веб-страница, которая представлена в виде HTML-кода, преобразуется в структуру, подобную той, которая показана на рисунке ниже.

Как работает JS: движки рендеринга веб-страниц и советы по оптимизации их производительности - 4

Дерево DOM

Каждый элемент этого дерева, содержащий вложенные элементы, является для них родительским. Это справедливо для всех уровней вложенности.

Создание дерева CSSOM

CSSOM (CSS Object Model) — это объектная модель CSS. Когда браузер занимается созданием дерева DOM страницы, он находит в разделе head тег link, который ссылается на внешний CSS-файл, скажем, имеющий имя theme.css. Ожидая, что этот ресурс может понадобиться ему для рендеринга страницы, браузер выполняет запрос на загрузку данного файла. Этот файл содержит в себе обычный текст, представляющий собой описание стилей, применяемых к элементам страницы.

Как и в случае с HTML, движку нужно конвертировать CSS в нечто, с чем может работать браузер — в CSSOM. В результате получается дерево CSSOM, представленное на следующем рисунке.

Как работает JS: движки рендеринга веб-страниц и советы по оптимизации их производительности - 5

Дерево CSSOM

Знаете, почему CSSOM имеет древовидную структуру? Когда выполняется формирование итогового набора стилей для элемента страницы, браузер начинает с наиболее общих правил, применимых к этому элементу, представленному узлом DOM (например, если узел является потомком элемента body, к нему применяются все стили, заданные для body), а затем рекурсивно уточняет вычисленные стили, применяя более специфические правила.

Разберём пример, который представлен на предыдущем рисунке. Любой текст, содержащийся внутри тега span, который помещён в элемент body, выводится красным цветом и имеет размер шрифта, равный 16px. Эти стили унаследованы от элемента body. Если элемент span является потомком элемента p, значит его содержимое не выводится в соответствии с применённым к нему более специфичным стилем.

Кроме того, обратите внимание на то, что вышеприведённое дерево не является полным CSSOM-деревом. Тут показаны лишь стили, которые мы, в нашем CSS-файле, решили переопределить. У каждого браузера имеется стандартный набор стилей, применяемый по умолчанию, известный ещё как «стили пользовательского агента» (user agent styles). Именно результаты применения этих стилей можно видеть на странице, не имеющей связанных с ней CSS-правил. Наши же стили просто переопределяют некоторые из стандартных стилей браузера.

Создание дерева рендеринга

Инструкции о внешнем виде элементов, представленные в HTML, скомбинированные с информацией об их стилизации из дерева CSSOM, используются для формирования дерева рендеринга.

Что это такое? Это — дерево визуальных элементов, созданных в том порядке, в котором они будут выводиться на экран. Это — визуальное представление HTML-кода страницы, отражающее влияние соответствующих этой странице CSS-правил. Цель этого дерева заключается в том, чтобы обеспечить вывод элементов правильном порядке.

Узел дерева рендеринга известен в движке WebKit как «renderer» или «render object» (мы будем называть их «объектами рендеринга»).

Вот как будет выглядеть дерево рендеринга для деревьев DOM и CSSOM, показанных выше.

Как работает JS: движки рендеринга веб-страниц и советы по оптимизации их производительности - 6

Дерево рендеринга

Вот общее описание действий браузера, выполняемых им при создании дерева рендеринга.

  • Начиная с корня дерева DOM, браузер обходит каждый видимый узел. Некоторые узлы невидимы (например — теги, содержащие ссылки на скрипты, мета-теги, и так далее), их браузер пропускает, так как они не влияют на внешний вид страницы. Некоторые узлы скрыты средствами CSS, браузер так же не включает их в дерево рендеринга. Например, узел span из нашего примера не выводится в дереве рендеринга, так как у нас имеется явным образом заданное правило, устанавливающего для него свойство display: none.
  • Для каждого видимого узла браузер находит подходящие CSSOM-правила и применяет их.
  • В результате формируется структура, содержащая видимые узлы и вычисленные для них стили.

Для того, чтобы лучше понять то, о чём тут идёт речь, можете взглянуть на исходный код класса RenderObject из WebKit. Каждый объект рендеринга представляет собой прямоугольную область, обычно соответствующую CSS-блоку узла. Сведения об этом блоке включают его геометрические характеристики, такие, как ширина, высота и позиция.

Формирование макета страницы

После того, как объект рендеринга создан и добавлен в дерево, ему пока ещё не назначены позиция и размер. Вычисление этих значений и называется формированием макета страницы.

HTML использует потоковую модель компоновки. Это означает, что чаще всего система может вычислить геометрические параметры элементов за один проход. Тут используется координатная система, основанная на корневом объекте рендеринга, в ней применяются координаты left и top.

Формирование макета — это рекурсивный процесс. Он начинается в корневом объекте, который соответствует элементу документа <html>. Процесс выполняется рекурсивно по всей иерархической структуре объекта рендеринга, производится вычисление размеров и положения для каждого элемента, который в этом нуждается.

Позиция корневого объекта рендеринга — 0,0. Его размеры соответствуют размерам видимой части окна браузера (это называют «областью просмотра», viewport).

Процесс формирования макета означает задание каждому узлу точной позиции, в которой он должен появиться на странице.

Визуализация дерева рендеринга

На данном этапе осуществляется обход дерева рендеринга и вызов методов paint() объектов рендеринга, которые и выполняют вывод графического представления объектов на экран.

Визуализация, или отрисовка, может быть глобальной или инкрементной (так же выполняется и формирование макета страницы).

  • Глобальная отрисовка означает повторный вывод всего дерева рендеринга.
  • Инкрементная отрисовка выполняется в ситуации, когда меняются лишь некоторые из объектов рендеринга, причём так, что это не влияет на всё дерево. Подсистема рендеринга делает недействительными прямоугольные области на экране. Это приводит к тому, что операционная система воспринимает их как участки, содержимое которых нужно обновить и сгенерировать для них событие paint. Операционная система выполняет перерисовку областей интеллектуально, объединяя несколько областей в одну.

В целом, важно понимать, что визуализация — это поэтапный процесс. Для улучшения восприятия страницы пользователями движок рендеринга стремится к тому, чтобы вывести страницу на экран как можно скорее. Он не будет ждать до тех пор, пока будет разобран весь HTML, для того, чтобы приступить к формированию дерева рендеринга и к расчёту параметров макета страницы. В результате некоторые части страницы окажутся обработанными и выведенными на экран, в то время как движок рендеринга продолжит работу с оставшимся содержимым страницы, которое поступает из сети.

Порядок обработки JS-скриптов и CSS-файлов

Разбор и выполнение скрипта осуществляется сразу же после того, как система обработки кода страницы достигнет тега <script>. Обработка документа приостанавливается до тех пор, пока скрипт не будет выполнен. Это означает, что данный процесс выполняется синхронно.

Если скрипт получают из внешнего источника, то сначала он должен быть загружен через сеть (тоже синхронно). Обработка страницы приостанавливается до тех пор, пока загрузка скрипта не будет завершена.

HTML5 позволяет указывать на возможность асинхронной загрузки и обработки скрипта с использованием отдельного потока.

Оптимизация производительности рендеринга

Если вы хотите оптимизировать своё приложение с учётом особенностей рендеринга страниц, существует пять основных областей, которые вы можете контролировать, и на которые нужно обратить внимание.

  • JavaScript. В предыдущих материалах этой серии мы рассказывали о том, как писать оптимизированный JS-код, не блокирующий пользовательский интерфейс, эффективно использующий память и реализующий другие полезные техники. Когда речь идёт о рендеринге, нам нужно учитывать то, как JS-код будет взаимодействовать с элементами DOM на странице. JavaScript может вносить множество изменений в пользовательский интерфейс, особенно если речь идёт об одностраничных приложениях.
  • Вычисление стилей. Это — процесс определения того, какое CSS-правило применяется к конкретному элементу с учётом соответствующих этому элементу селекторов. После определения правил осуществляется их применение и вычисление итогового стиля для каждого элемента.
  • Формирование макета страницы. После того, как браузер узнает о том, какие стили применяются к элементу, он может приступить к вычислению того, как много места на экране займёт этот элемент, и к нахождению его позиции. Модель макета веб-страницы указывает на то, что одни элементы могут влиять на другие элементы. Например, ширина элемента <body> может влиять на ширину дочерних элементов, и так далее. Всё это означает, что процесс формирования макета — это задача, требующая интенсивных вычислений. Кроме того, вывод элементов выполняется на множество слоёв.
  • Отрисовка. Именно здесь выполняется преобразование всего, что было вычислено ранее, в пиксели, выводимые на экран. Этот процесс включает в себя вывод текста, цветов, изображений, границ, теней, и так далее. Речь идёт о каждой видимой части каждого элемента.
  • Компоновка. Так как части страницы вполне могут быть выведены на различных слоях, их требуется совместить в едином окне в нужном порядке, что приведёт к правильному выводу страницы. Это очень важно, особенно — для перекрывающихся элементов.

Оптимизация JS-кода

JavaScript-код часто приводит к изменению того, что можно наблюдать в браузере. Особенно это актуально для одностраничных приложений. Вот несколько советов, касающихся оптимизации JS для улучшения процесса рендеринга страниц.

  • Избегайте использования функций setTimeout() и setInterval() для обновления внешнего вида элементов страниц. Эти функции вызывают коллбэк в некоторый момент формирования кадра, возможно, в самом конце. Нам же нужно вызвать команду, приводящую к визуальным изменениям, в начале кадра, и не пропустить его.
  • Переносите длительные вычисления в веб-воркеры.
  • Используйте для выполнения изменений в DOM микро-задачи, разбитые на несколько кадров. Этим следует пользоваться тогда, когда задача нуждается в доступе к DOM, а доступ к DOM, из веб-воркера, например, получить нельзя. Это означает, что большую задачу нужно разбить на более мелкие и выполнять их внутри requestAnimationFrame, setTimeout, или setInterval, в зависимости от особенностей задачи.

Оптимизация CSS

Модификация DOM путём добавления и удаления элементов, изменения атрибутов и других подобных действий приведёт к тому, что браузеру придётся пересчитать стили элементов, и, во многих случаях, макет всей страницы, или, по крайней мере, некоторой её части. Для оптимизации процесса рендеринга страницы учитывайте следующее.

  • Уменьшите сложность селекторов. Использование сложных селекторов может привести к тому, что работа с ними займёт более 50% времени, необходимого для вычисления стилей элемента, остальное время уйдёт на конструирование самого стиля.
  • Уменьшите число элементов, для которых нужно выполнять вычисление стилей. То есть, лучше, если изменение стиля будет направлено на несколько элементов, а не на всю страницу.

Оптимизация макета

Пересчёт макета страницы может требовать серьёзных системных ресурсов. Для оптимизации этого процесса примите во внимание следующее.

  • Уменьшите число ситуаций, приводящих к пересчёту макета. Когда вы меняете стили, браузер выясняет, требуется ли пересчёт макета для отражения этих изменений. Изменения свойств, таких, как ширина, высота, или позиция элемента (в целом, речь идёт о геометрических характеристиках элементов), требуют изменения макета. Поэтому, без крайней необходимости, не меняйте подобные свойства.
  • Всегда, когда это возможно, используйте модель flexbox вместо более старых моделей создания макетов. Эта модель работает быстрее, чем другие, что может дать значительный прирост производительности.
  • Избегайте модели работы с документом, предусматривающей периодическое изменение параметров элементов и их последующее считывание. В JavaScript доступны параметры элементов DOM (вроде offsetHeight или offsetWidth) из предыдущего кадра. Считывание этих параметров проблем не вызывает. Однако, если вы, до чтения подобных параметров, меняете стиль элемента (например, динамически добавляя к нему какой-то CSS-класс), браузеру потребуется потратить немало ресурсов для того, чтобы применить изменения стиля, создать макет и возвратить в программу нужные данные. Это может замедлить программу, подобного стоит избегать всегда, когда это возможно.

Оптимизация отрисовки

Часто эта задача отнимает больше всего времени, поэтому важно избегать ситуаций, приводящих к перерисовке страницы. Вот что здесь можно сделать.

  • Изменение любого свойства, за исключений трансформаций и изменений прозрачности, приводит к перерисовке. Используйте эти возможности умеренно.
  • Если ваши действия вызвали пересчёт макета, это приводит и к вызову перерисовки страницы, так как изменения геометрических параметров элемента ведут и к его визуальным изменениям.
  • Уменьшайте области страниц, которые необходимо перерисовывать, грамотно управляя расположением слоёв и анимацией.

Итоги

В этом материале мы поговорили о подсистемах рендеринга современных браузеров. Правильный подход к визуализации страниц ведёт к повышению производительности веб-приложений и к улучшению впечатлений пользователей от работы с ними.

Предыдущие части цикла статей:

Часть 1: Как работает JS: обзор движка, механизмов времени выполнения, стека вызовов
Часть 2: Как работает JS: о внутреннем устройстве V8 и оптимизации кода
Часть 3: Как работает JS: управление памятью, четыре вида утечек памяти и борьба с ними
Часть 4: Как работает JS: цикл событий, асинхронность и пять способов улучшения кода с помощью async / await
Часть 5: Как работает JS: WebSocket и HTTP/2+SSE. Что выбрать?
Часть 6: Как работает JS: особенности и сфера применения WebAssembly
Часть 7: Как работает JS: веб-воркеры и пять сценариев их использования
Часть 8: Как работает JS: сервис-воркеры
Часть 9: Как работает JS: веб push-уведомления
Часть 10: Как работает JS: отслеживание изменений в DOM с помощью MutationObserver

Уважаемые читатели! Какие приёмы вы применяете для оптимизации рендеринга страниц ваших веб-проектов?

Как работает JS: движки рендеринга веб-страниц и советы по оптимизации их производительности - 7

Автор: ru_vds

Источник

Поделиться

* - обязательные к заполнению поля