10 февраля 2020

В современном вебе, где сайты в среднем отправляют 500КБ сжатого JavaScript и 1,5 МБ изображений, работая при этом на Android-устройстве среднего уровня через 3G с лагом в 400мс, производительность CSS-селекторов является, наверное, наименьшей из существующих проблем.

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

Основы парсинга CSS

Во-первых, чтобы избежать непонимания в дальнейшем — эта статья не касается производительности CSS-свойств и их значений. Здесь мы рассматриваем эксплуатационные затраты самих селекторов. Я сосредоточусь на движке рендеринга Blink, а именно на браузере Chrome версии 62.

Селекторы можно разделить на несколько групп и (грубо) отсортировать их по возрастанию затрат на их обработку:

Тип селектораПример
1.Идентификатор (ID)#classID
2.Класс (Class).class
3.Тег (Tag)div
4.Обобщенный и cоседний родственный комбинатор (General and adjacent sibling)div ~ a, div + a
5.Наследственный и потомственный комбинатор (Child and descendant)div > a, div a
6.Универсальный (Universal)*
7.Атрибут (Attribute)[type="text"]
8.Псевдоклассы и элементы (Pseudo-classes and elements)a:first-of-type, a:hover

Означает ли это, что нужно использовать только ID и классы? Ну не совсем. Зависит от ситуации. Во-первых, давайте рассмотрим, как браузеры интерпретируют CSS-селекторы.

Браузеры читают CSS справа налево. В составном селекторе самый правый селектор называется ключевым. Так, например, в #id .class> ul a, ключевым селектором является a. Сначала браузер ищет именно ключевые селекторы. В нашем примере он находит все элементы на странице, которые соответствуют селектору a. Затем он находит все элементы ul на странице и фильтрует элементы a оставляя только те, которые являются потомками элементов ul, и так далее, пока не достигнет крайнего левого селектора.

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

Измерение производительности

Бен Фрейн создал серию тестов для измерения производительности различных селекторов еще в 2014 году. Тестирование заключалось в измерении скорости, необходимой для анализа различных селекторов, начиная с простых идентификаторов (ID) и заканчивая некоторыми, действительно сложными и длинными, составными селекторами. Измерение производилось на огромном DOM, состоящем из 1000 идентичных элементов. В итоге он обнаружил, что разница между самым медленным и быстрым селектором составляла ~15мс.

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

Я решил сделать свои собственные тесты и для этого я использовал тест Пола Льюиса, упомянутый в одном комментарии, выражающем озабоченность полезными, но чрезмерно замысловатыми «количественными CSS-селекторами»:

Эти селекторы являются одними из самых медленных. Примерно в 500 раз медленнее, чем что-то дикое, типа "div.box:not(:empty):last-of-type .title". Страница с тестом http://jsbin.com/gozula/1/quiet

Тест был немного изменен — количество элементов увеличено до 50000, и вы можете проверить его сами. Я сделал в среднем 10 прогонов на моем MacBook Pro 2014, и я получил следующие результаты:

СелекторВремя выполнения (ms)
div4.8740
.box3.625
.box > .title4.4587
.box .title4.5161
.box ~ .box4.7082
.box + .box4.6611
.box:last-of-type3.944
.box:nth-of-type(2n - 1)16.8491
.box:not(:last-of-type)5.8947
.box:not(:empty):last-of-type .title8.0202
.box:nth-last-child(n+6) ~ div20.8710

Разумеется, результаты будут варьироваться в зависимости от того, используете ли вы метод querySelector или метод querySelectorAll, а также от количества совпадающих узлов (nodes) на странице, но использование querySelectorAll ближе к реальному варианту использования CSS, который ориентирован на все совпадающие элементы.

Даже при таких экстремальных условиях, когда на странице находится 50000 совпадающих элементов и используются действительно "безумные" селекторы, типа последнего в таблице выше, мы обнаруживаем, что самый медленный селектор обрабатывается примерно 20мс, самый быстрый — простой класс — примерно 3,5мс. Не такая уж и существенная разница. В реалистичном и менее экстремальном DOM, насчитывающем от 1000 до 5000 узлов, можно ожидать падения времени обработки примерно в 10 раз, доведя время парсинга до нескольких миллисекунд.

Из этого теста можно сделать вывод о том, что беспокоиться о производительности CSS селекторов не стоит. Просто не стоит переусердствовать с псевдоселекторами и действительно длинными составными селекторами. Мы также видим, как за последние два года улучшился движок Blink. Вместо указанного в цитируемом комментарии замедления в ~500 раз при обработке "количественных CSS-селекторов" (.box: nth-last-child (n + 6) ~ div) по сравнению с "безумными селекторами" (.box:not(:empty):last-of-type .title) мы видим падение производительности всего в 1,5 раза. Это поразительное улучшение. При этом следует ожидать, что браузеры будут и дальше улучшаться в этом направлении, что еще больше снизит производственные затраты при работе с CSS-селекторами.

Тем не менее по возможности вы должны использовать классы, а также принять какие-либо правила именования (методологию) типа BEM ("Block, element, modifier" — "Блок, элемент, модификатор"), SMACSS ("Scalable and Modular Architecture for CSS" — "Масштабируемая и модульная архитектура для CSS") или OOCSS ("Object-oriented CSS" — "Объектно-ориентированный CSS"), поскольку это не только улучшит производительность вашего веб-сайта, но и значительно облегчит поддержку кода. Слишком сложные составные селекторы, особенно использующие теги и универсальные селекторы — например, .header nav ul > li a > .inner — чрезвычайно хрупки и являются источником многих непредвиденных ошибок. Они также являются настоящим кошмаром при поддержке кода, особенно если он достался вам от кого-то другого.

Качество важнее количества

Хуже наличия "дорогих" селекторов является только большое их количество. Это явление известно как "раздутые стили" и, вероятно, вы встречали эту проблему в своей практике. Типичными примером являются сайты, которые целиком импортируют различные CSS-фреймворки (типа Bootstrap или Foundation), используя при этом менее 10% их кода. Другой пример — это старые, никогда не переживавшие рефакторинг, проекты, чей CSS-код перешел в состояние "хронологические таблицы стилей" — т.е. CSS с кучей классов, добавленных в конец файла по мере роста проекта, который со временем стал похож на заброшенный сад.

Кроме того, что передача большого файла стилей по сети занимает много времени (а сеть является самым узким местом в производительности веб-сайта), большие CSS-файлы также дольше парсятся. А ведь помимо построения DOM вашего HTML документа, браузеру необходимо построить CSSOM (объектную модель CSS), чтобы сравнить его с DOM и сопоставить селекторы.

Итак, держите свои стили "стройными", не добавляйте в них что попало, загружайте только то, что вам нужно, и только тогда, когда это вам нужно. Кроме того, по необходимости пользуйтесь различными инструментами для выявления неиспользуемого CSS-кода (типа UNCSS).

Для получения более детальной информации о том, как браузеры парсят CSS, посмотрите запись Николь Салливан о движке Webkit, статью Ильи Григорика о движке Blink или статью Лин Кларк о новом CSS-движке Mozilla (aka Stylo).

Очевидная проблема: инвалидация стилей

Мы рассмотрели хорошие примеры зависимости скорости обработки CSS-селекторов от их сложности, однако это всё касается единоразового рендеринга. Но сегодняшние веб-сайты уже не являются статическими документами, а больше напоминают приложения с динамическим контентом, с которым пользователи могут взаимодействовать. Это всё немного усложняет, поскольку синтаксический анализ CSS-кода — это всего лишь один шаг в конвейере рендеринга браузера. Вот более детальный (рендеринг-ориентированный) взгляд на то, как браузер отображает кадр на экране (из статьи "Производительность визуализации" на developers.google.com):

Мы опустим вопрос производительности JavaScript и композитинг ("Composite" на рисунке выше), а сосредоточимся на фиолетовой части — парсинге стилей ("Style") и компоновке элементов ("Layout").

После построения DOM и CSSOM, браузеру необходимо объединить их в дерево рендеринга, прежде чем начать отрисовку на экране. На этом этапе браузеру необходимо вычислить окончательный набор CSS свойств и их значений для каждого найденного элемента. Вы сами можете это наблюдать в панели Elements > Styles инструментов разработчика браузера. Браузер берет все, соответствующие определенному элементу стили, включая специфические, свойственные самому браузеру (так называемые user agent stylesheet) для создания окончательного, вычисленного (computed) стиля элемента.

Затем браузер может перейти к этапу компоновки (также известному как reflow), где он вычисляет геометрию и создает полевую модель страницы, размещая каждый элемент на соответствующей ему позиции в окне просмотра. Компоновка является наиболее вычислительно-интенсивной частью этого процесса.

Наконец, на этапе отрисовки, браузер преобразует каждый узел дерева рендеринга в фактические пиксели на экране.

Теперь о том, что происходит, когда мы видоизменяем DOM, изменяя некоторые классы на странице, добавляя или удаляя некоторые узлы, изменяя некоторые атрибуты или каким-либо другим образом "балуясь" с HTML (или самими стилями)?

Мы отменяем вычисленные ранее стили, и браузеру необходимо аннулировать всё вниз по дереву совпадающих селекторов. И хотя современные браузеры стали значительно умнее, раньше, в ситуации, когда вы изменяли класс элемента <body>, браузеру приходилось пересчитывать стили всем его элементам-потомкам.

Один из способов избежать этой проблемы — уменьшить сложность ваших селекторов. Вместо использования таких вариантов как #nav > .list > li > a, используйте один селектор, например .nav-link. Таким образом, вы уменьшите объём отменяемых стилей, так как теперь, если вы измените что-либо внутри #nav, это не приведёт к пересчету стилей для всего узла.

Другой способ — уменьшить область действия — например, количество отменяемых (требующих пересчёта стилей) элементов. Будьте конкретны в вашем CSS. Имейте это в виду, особенно при использовании анимации, где браузеру даётся всего ~10мс*, чтобы выполнить всю необходимую работу.

Более детально изучить проблему инвалидации стилей можно прочитав статью Style Invalidation in Blink.

Заключение

Подводя итоги, можно сказать, что беспокоиться о производительности CSS-селекторов не стоит, если только вы действительно не переходите границы. Несмотря на то, что в 2012 году эта тема была актуальной, с тех пор браузеры стали намного быстрее и умнее. Даже Google больше не беспокоится об этом. Если вы посмотрите страницу PageSpeed Insights Rules, вы не увидите там правила «Use efficient CSS selectors» — «Используйте эффективные CSS-селекторы», которое было удалено оттуда в 2013 году.

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

И последнее замечание: всегда делайте свои собственные тесты. Не верьте тому, что было написано кем-то в Интернете несколько лет назад. Ситуация меняется кардинально и невероятно быстро. То, что актуально сегодня, может стать устаревшим раньше, чем вы это изучите.

 

 

* Имеется ввиду, что для гладкой анимации (с частотой 60 fps) на один кадр отводится 1000мс/60кадров ≈ 16,6мс. Отбросив время на саму отрисовку кадра, у браузера остаётся ~10мс для проведения всех вычислений. — прим. перев.^

Перевел scorp13

Статья является свободным переводом материала сайта https://www.sitepoint.com
Автор Ivan Čurić