CSS — это не чёрная магия

в 11:49, , рубрики: css, Блог компании RUVDS.com, Веб-разработка, Разработка веб-сайтов

Всем веб-программистам время от времени приходится писать CSS. Впервые с ним столкнувшись, вы, скорее всего, сочтёте, что понять CSS — это ерунда. И правда — тут добавили границы, там поменяли цвет… JavaScript — вот по-настоящему сложная штука. CSS по сравнению с ним — игрушка.

CSS — это не чёрная магия - 1

Однако, по мере того, как вы будете совершенствоваться в деле веб-разработки, легкомысленное отношение к CSS останется в прошлом. Столкнувшись с чем-то неописуемо странным, вы поймёте, что попросту не представляете, как именно CSS работает, что делается в его недрах. Что-то похожее было и у меня. Первые пару лет после учёбы я занималась JavaScript-разработкой полного цикла, эпизодически касаясь CSS. Я всегда считала, что мой хлеб — это JavaScript, ему я отдавала всё своё время.

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

Про CSS часто шутят, но немногие отнеслись к нему достаточно серьёзно и попытались его понять. Кто, столкнувшись с проблемой, осмысленно занимался поиском ошибок, учитывая то, как CSS обрабатывают браузеры? Вместо этого мы хватаем код из первого попавшегося ответа на Stack Overflow, не брезгуем разного рода хаками, или просто игнорируем проблемы.

Как результат — слишком часто разработчики попросту разводят руками, когда браузер творит со стилями непонятные вещи. Тут впору подумать, что CSS сродни чёрной магии. Однако, любому программисту известно, что компьютер — это машина для разбора и выполнения написанных человеком команд, и CSS, в этом плане, ничем не отличается от любимого нами JS. А если так — CSS вполне можно узнать и пользоваться им осмысленно и продуктивно.

Знание внутренних механизмов работы CSS пригодится в разных ситуациях. Например — при серьёзной отладке и оптимизации производительности веб-страниц. Часто, когда говорят и пишут про CSS, делают упор на решение распространённых проблем. Мне же хочется рассказать о том, как всё это работает.

DOM и CSSOM

Для начала, важно понимать, что в браузерах, помимо прочих подсистем, имеется JavaScript-движок и движок рендеринга. Нас, в данном случае, интересует последний. Например, мы обсудим детали, которые относятся к WebKit (Safari), Blink (Chrome), Gecko (Firefox), и Trident/EdgeHTML (IE/Edge).

Браузер, в ходе вывода веб-страницы на экран, выполняет определённую последовательность действий, ведущую к построению объектной модели документ (DOM, Document Object Model), и объектной модели таблицы стилей (CSSOM, CSS Object Model). Эту последовательность действий, упрощённо, можно представить в следующем виде:

  • Преобразование: чтение байтов HTML- и CSS-кода с диска или загрузка их из сети.
  • Токенизация: разбивка потока входных данных на фрагменты (например: начальные теги, конечные теги, имена атрибутов, значения атрибутов), удаление ненужных символов, таких, как пробелы и переводы строк.
  • Лексирование: этот шаг похож на токенизацию, но здесь осуществляется определение типа каждого токена (например: этот токен — число, тот — строковой литерал, ещё один — оператор равенства).
  • Синтаксический анализ: здесь система принимает поток токенов после лексирования, интерпретирует их, используя специфическую грамматику, и превращает этот поток в абстрактное синтаксическое дерево.

После того, как будут созданы подобные древовидные структуры для CSS и HTML, движок рендеринга формирует, основываясь на них, то, что называется деревом рендеринга. Это — часть процесса создания макета документа.

Дерево рендеринга — это модель визуального представления документа, которая позволяет вывести графические элементы документа в правильном порядке. Конструирование дерева рендеринга производится по такому алгоритму:

  • Начиная с корня дерева DOM обойти все видимые узлы.
  • Пропустить невидимые узлы.
  • Для каждого видимого узла найти соответствующие ему правила в CSSOM и применить их.
  • Сгенерировать видимые узлы с содержимым и вычисленными для них стилями.
  • И, наконец, сформировать дерево рендеринга, которое включает в себя, для всех видимых на экране элементов страницы, и их содержимое, и информацию о стилях.

CSSOM может оказать сильнейшее влияние на дерево рендеринга, но не на дерево DOM.

Рендеринг

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

  • Создание макета страницы. Этот этап включает в себя вычисление размеров элемента и его позиции на экране. Родительские элементы могут воздействовать на дочерние элементы. Иногда и дочерние элементы могут воздействовать на родительские.
  • Отрисовка. В ходе этого этапа производится преобразование дерева рендеринга в изображения, которые являются основой того, что будет выведено на экран. Сюда входит вывод текстов, цветов, изображений, границ, теней. Отрисовка обычно выполняется на нескольких слоях, при этом, если JavaScript-код страницы воздействует на DOM, страница может быть перерисована несколько раз.
  • Сведение слоёв. На данном этапе производится объединение слоёв и формирование итогового изображения, которое и будет видимо на экране. При этом, так как элементы страницы могут быть выведены на разных слоях, слои нужно свести в правильном порядке.

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

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

Говоря о выводе графического представления страниц в браузерах, часто упоминают и аппаратное ускорение графики. В подобных случаях обычно имеют в виду ускорение сведения слоёв. Речь идёт об использовании ресурсов видеокарты для подготовки к выводу содержимого веб-страниц.

Сведение слоёв с использованием аппаратного ускорения позволяет значительно увеличить скорость рендеринга в сравнении с традиционным подходом, при котором используется лишь процессор. В связи со всем этим нельзя не вспомнить о CSS-свойстве will-change, умелое использование которого позволяет ускорить вывод страниц. Например, при использовании CSS-трансформаций, свойство will-change позволяет подсказать браузеру, что элемент DOM будет трансформирован в ближайшем будущем. Выглядит это как will-change: transform. Это позволяет передать GPU некоторые операции по отрисовке и сведению слоёв, что способно значительно повысить производительность страниц, содержащих много анимированных элементов. Улучшить производительность с помощью will-change можно, воспользовавшись конструкциями will-change: scroll-position, will-change: contents, will-change: opacity, will-change: left, top.

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

Например, изменение цвета элемента оставит макет неизменным, приведя лишь к перерисовыванию элемента. А вот изменение позиции элемента приведёт и к изменению макета, и к перерисовке самого элемента, его дочерних элементов, и, возможно, смежных элементов. Добавление узла DOM так же приведёт к пересчёту макета и к перерисовыванию страницы. Серьёзные изменения, такие, как увеличение размера шрифта HTML-элемента, приводят к изменению макета и перерисовыванию всего дерева рендеринга.

Если вы похожи на меня, то вы, вероятно, лучше знакомы с DOM, чем с CSSOM, поэтому давайте уделим CSSOM некоторое внимание. Важно отметить, что по умолчанию CSS рассматривается как ресурс, блокирующий рендеринг. Это означает, что браузер не будет выполнять рендеринг до полной готовности CSSOM.

Кроме того, надо понимать, что между DOM и CSSOM нет полного соответствия. Различия этих структур обусловлены наличием в DOM невидимых элементов, скриптов, мета-тегов, тегов со служебной информацией, не выводимой на экран, и так далее. Всё это при построении CSSOM не учитывается, так как не влияет на графическое представление страницы.

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

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

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

Браузер выполняет HTTP-запрос, запрашивая у сервера страницу. Веб-сервер отправляет ответ. Браузер конвертирует данные, полученные от сервера, в токены, которые затем преобразуется в узлы деревьев DOM и CSSOM. После того, как эти деревья готовы, строится дерево рендеринга, на основе которого формируется макет страницы, производится послойная отрисовка элементов и сведение слоёв. В результате получается веб-страница, которую мы видим на экране.

Специфичность селекторов

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

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

Расчёт показателей специфичности селекторов сбивает с толку многих JavaScript-разработчиков, поэтому давайте остановимся на этом подробнее. Мы будем использовать следующий пример. Имеется тег div с классом container. В этот тег вложен ещё один div, id которого — main. Внутри main имеется тег p, в котором содержится тег a.

<div class="container">
  <div id="main">
    <p>
      <a href="">Hello!</a>
    </p>
  </div>
</div>

Сейчас, не подглядывая в ответ, попытайтесь проанализировать нижеприведённый CSS и сказать, какого цвета будет текст ссылки в теге a.

#main a { 
  color: green;
}
p a { 
  color: yellow;
}
.container #main a {
  color: pink;
}
div #main p a { 
  color: orange;
}
a { 
  color: red;
}

Может, красного цвета? Или зелёного? Нет. Ссылка будет розового цвета со значением специфичности 1,1,1. Вот остальные результаты:

  • div #main p a: 1,0,3
  • #main a: 1,0,1
  • p a: 2
  • a: 1

Для нахождения этих чисел нужно произвести следующие вычисления:

  • Первое число: количество селекторов ID.
  • Второе число: количество селекторов класса, селекторов атрибутов (например: [type="text"], [rel="nofollow"]) и псевдоклассов (:hover, :visited).
  • Третье число: количество селекторов типа и псевдоэлементов (::before, ::after)

Например, взглянем на такой селектор:

#header .navbar li a:visited

Значение специфичности для него будет 1,2,2. Тут имеется один ID, один класс, один псевдокласс, и два селектора типа элемента (li и a). Это значение можно прочитать и так, как будто в нём нет запятых, вместо 1,2,2 — 122. Запятые тут имеются только для того, чтобы подчеркнуть, что перед нами не десятичное число из трёх цифр, а три числа. Это особенно важно для теоретически возможных результатов вроде 0,1,13. Если переписать это в виде 0113, неясно будет, как вернуть его в исходное состояние.

Позиционирование элементов

Теперь мне хотелось бы поговорить о позиционировании. Позиционирование элементов и создание макета страницы, как мы уже видели, идут рука об руку.

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

Часто меня спрашивают о преимуществах и недостатках использования flexbox и float. Конечно, flexbox — это, с точки зрения юзабилити, очень хорошо, будучи применённым к одному и тому же элементу, макет с flexbox рендерится примерно 3.5 мс, в то время как рендеринг макет с float может занять около 14 мс. Таким образом, для учёта мелких, но важных деталей, JS-разработчикам имеет смысл поддерживать свои знания в области CSS в таком же хорошем состоянии, как и знания в области JavaScript.

Свойство z-index

И, наконец, мне хотелось бы поговорить о свойстве z-index. На первый взгляд кажется, что говорить тут особо не о чем. Каждый элемент в HTML-документе может быть либо перед другими, либо позади них. Кроме того, это работает только для позиционированных элементов. Если установить свойство z-index для элемента, позиционирование которого явно не задано, это ничего не изменит.

Ключ к поиску и устранению проблем, связанных с z-index, заключается в понимании идеи контекстов наложения. Поиск неполадок всегда начинается с корневого элемента контекста наложения. Контекст наложения — это концепция расположения HTML-элементов в трёхмерном пространстве, в частности — вдоль оси Z, относительно пользователя, находящегося перед монитором. Другими словами — это группа элементов с общим родителем, которые вместе перемещаются по оси Z либо ближе к пользователю, либо дальше от него.

Каждый контекст наложения имеет один HTML-элемент в качестве корневого элемента. Когда свойства позиционирования и z-index не используются, правила взаимодействия элементов просты. Порядок наложения элементов соответствует порядку их появления в HTML.

Однако, можно создавать новые контексты наложения с помощью свойств, отличающихся от z-index, и вот тут уже всё становится сложнее. Среди них — свойство opacity, когда это значение меньше единицы, filter, когда значение этого свойства отличается от none, и mix-blend-mode, значение которого не normal. Эти свойства, на самом деле, создают новые контексты наложения. На всякий случай хочется напомнить, что режим наложения (blend mode) позволяет задать то, как пиксели на некоем слое взаимодействуют с видимыми пикселями на слоях, расположенных ниже этого слоя.

Свойство transform тоже вызывает создание нового контекста наложения в тех случаях, когда оно отличается от none. Например, scale(1) и translate3d(0,0,0). Опять же, как напоминание, свойство scale используется для изменения размера элемента, а translate3d позволяет задействовать GPU для CSS-переходов, повышая качество анимации.

Итоги

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

Уважаемые читатели! Если вы хорошо знаете CSS, расскажите пожалуйста о том, как вы с ним разобрались. Поделитесь опытом. Уверены, многим это пригодится.

Автор: ru_vds

Источник

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js