Возвращаемся к вопросу производительности CSS: селекторы, раздутые и тяжелые стили

Что такое быстрый CSS? Какие у него слабые места? Работают ли всё ещё правила быстрых и медленных селекторов? Являются ли используемые нами свойства более важными, чем селекторы? Думаю, пришло время пересмотреть некоторые из этих вопросов.

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

В дискуссиях относительно производительности CSS разработчики часто ссылаются на исследование по теме CSS-селекторов, опубликованное Стивом Саудерсом (Steve Souders) в 2009 году. Его используют как аргумент в пользу заявлений вроде «селекторы по атрибуту медленные» или «псевдоселекторы медленно работают».

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

Когда дело касается CSS, архитектура заключена вне скобок; а производительность — внутри

Бен Фрейн (Ben Frain)

Однако максимум, на что я был способен до сих пор — это в подтверждение своих догадок, что селекторы не так уж значимы, ссылаться на статью Николь Саливан (Nicole Sullivan) на сайте Performance Calendar. Но сам я никогда не проверял эту теорию: нехватка таланта и отсутствие склонности к аналитике удерживали меня от попыток.

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

Проверка скорости селекторов

В упомянутых ранее тестах Стива Саудерса используется JavaScript-конструктор new Date(). Однако современные браузеры (за исключением iOS/Safari) поддерживают API временных характеристик навигации (Navigation Timing API), который даёт нам более точные инструменты для измерения. Я буду применять его следующим образом:

<script type="text/javascript">
    ;(function TimeThisMother() {
        window.onload = function(){
            setTimeout(function(){
            var t = performance.timing;
                alert("Speed of selection is: " + (t.loadEventEnd — t.responseEnd) + " milliseconds");
            }, 0);
        };
    })();
</script>

Это позволяет нам измерить отрезок времени между моментом получения всех ресурсов (responseEnd) и моментом отображения страницы (loadEventEnd).

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

<div class="tagDiv wrap1">
  <div class="tagDiv layer1" data-div="layer1">
    <div class="tagDiv layer2">
      <ul class="tagUl">
        <li class="tagLi"><b class="tagB"><a href="/" class="tagA link" data-select="link">Select</a></b></li>
      </ul>
    </div>
  </div>
</div>

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

  1. Атрибут data
  2. Атрибут data (с указанием элемента)
  3. Атрибут data (без указания элемента, но с конкретным значением)
  4. Атрибут data (с указанием элемента и конкретным значением)
  5. Несколько атрибутов data (с указанием элементов и конкретными значениями)
  6. Один псевдоэлемент в селекторе (например :after)
  7. Комбинирование классов (например class1.class2)
  8. Несколько классов
  9. Несколько классов с дочерним селектором
  10. Частичное совпадение по атрибуту (например [class^=“wrap”])
  11. Селектор nth-child
  12. Селектор nth-child, за которым следует ещё один селектор nth-child
  13. Бредовый селектор (указана вся иерархия, каждый класс в цепочке, например div.wrapper > div.tagDiv > div.tagDiv.layer2 > ul.tagUL > li.tagLi > b.tagB > a.TagA.link)
  14. Полубредовый селектор (например .tagLi .tagB a.TagA.link)
  15. Универсальный селектор
  16. Селектор по элементу
  17. Контекстный селектор
  18. Контекстный селектор из трёх элементов
  19. Контекстный селектор с псевдоэлементом
  20. Селектор по классу

Тест был запущен по 5 раз в каждом браузере, и полученные результаты были сведены к среднему значению. Использовались современные браузеры:

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

Хотите провести те же тесты самостоятельно? Нужные файлы в можно найти на GitHub: https://github.com/benfrain/css-performance-tests. Просто откройте каждую страницу в выбранном вами браузере (помните, что для вывода результатов браузер должен поддерживать API временных характеристик навигации). Также следует знать, что выполняя тесты я не учитывал первую пару результатов, так как в некоторых браузерах они оказывались невероятно высокими.

Учитывая результаты, я не думаю, что сравнение браузеров что-либо нам расскажет. Не в этом цель тестов. Их цель — попытаться оценить относительную разницу в скорости работы разных селекторов, которые мы использовали. Поэтому логичнее смотреть таблицу сверху вниз — по колонкам, а не по строкам.

Вот результаты. Время указано в миллисекундах:

Тест Chrome 34 Firefox 29 Opera 19 IE9 Android 4
1 56,8 125,4 63,6 152,6 1455,2
2 55,4 128,4 61,4 141 1404,6
3 55 125,6 61,8 152,4 1363,4
4 54,8 129 63,2 147,4 1421,2
5 55,4 124,4 63,2 147,4 1411,2
6 60,6 138 58,4 162 1500,4
7 51,2 126,6 56,8 147,8 1453,8
8 48,8 127,4 56,2 150,2 1398,8
9 48,8 127,4 55,8 154,6 1348,4
10 52,2 129,4 58 172 1420,2
11 49 127,4 56,6 148,4 1352
12 50,6 127,2 58,4 146,2 1377,6
13 64,6 129,2 72,4 152,8 1461,2
14 50,2 129,8 54,8 154,6 1381,2
15 50 126,2 56,8 154,8 1351,6
16 49,2 127,6 56 149,2 1379,2
17 50,4 132,4 55 157,6 1386
18 49,2 128,8 58,6 154,2 1380,6
19 48,6 132,4 54,8 148,4 1349,6
20 50,4 128 55 149,8 1393,8
Наибольшая разница 16 13,6 17,6 31 152
Самый медленный 13 6 13 10 6

Разница между самым быстрым и самым медленным селектором

Колонка «Наибольшая разница» показывает разницу в миллисекундах между самым быстрым и самым медленным селектором. Среди десктопных браузеров выделяется IE9 с наибольшей разницей между ними — 31 мс. В остальных разница составляет примерно половину от этой цифры. Однако, что интересно, браузеры не пришли к единому мнению относительно того, какой селектор является самым медленным.

Самый медленный селектор

Мне показалось любопытным то, что самый медленный селектор для всех браузеров свой. Для Оперы и Хрома самым медленным оказался «бредовый» селектор (тест 13). Их солидарность в этом вопросе не удивительна, учитывая, что они используют один движок — Blink. Firefox наибольшие усилия пришлось приложить для обработки селектора по псевдоэлементу (тест 6), та же история с устройством под управлением Android 4.2 (7" планшет hudl от Tesco). Ахиллесовой пятой Internet Explorer 9 оказался селектор с частичным совпадением по атрибуту (тест 10).

Правильный подход к архитектуре CSS

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

Что это значит?

Для меня результаты тестов стали подтверждением моей уверенности в том, что абсолютно незачем волноваться по поводу типа выбранного селектора. Даже сомневаться в правильности выбора селектора бессмысленно, так как мы видим, что все обработчики селекторов работают явно по-разному. Более того, разница между самым быстрым и самым медленным селектором не существенна даже при таком немыслимо огромном дереве документа. Как говорят у нас на севере Англии: «Для жарки есть рыбка и покрупнее».

После написания этой статьи со мной связался Бенджамин Пулейн (Benjamin Poulain), инженер WebKit, и высказал свои опасения по поводу используемой методологии. Его комментарии были очень интересными, и, с его разрешения, я процитирую некоторые из них:

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

Если взять временной график для [class^="wrap"] в качестве примера (получен на старом WebKit, так что в чем-то это будет похоже на Chrome), мы увидим:

~10% времени потрачено на прорисовку. ~21% времени — на первичную раскладку. ~48% времени потрачено на синтаксический разбор и построение дерева документа. ~8% — на определение стиля, ~5% — на сбор данных о стиле — это то, что мы должны протестировать, и что должно занимать большую часть времени. (Остальное время распределяется среди множества небольших функций).

В случае с предложенным выше тестом, представим, что у нас есть базовый показатель в 100 мс с самым быстрым селектором. Из них 5 мс уходит на сбор данных о стилях. Если другой селектор является в три раза более медленным, загрузка страницы займёт 110 мс. Тест должен давать в результате разницу в 300%, а мы получаем только 10%.»

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

«Я полностью согласен, что предварительная оптимизация селекторов бессмысленна, но по абсолютно другим причинам:

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

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

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

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

Причина и следствие

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

Как на быстроту применения селекторов повлияет большое количество неиспользуемых стилей?

Ещё один тест: я взял большую таблицу стилей с fiat.co.uk. Она состояла примерно из 3000 строчек кода. Все эти бесполезные стили были добавлены перед последним правилом, которое выбирало внутренний узел a.link и делало его красным. Как и в прошлый раз, я также усреднил результаты после 5 запусков теста в каждом браузере.

Затем я удалил половину этих правил и повторил тест для сравнения. Вот что получилось:

Тест Chrome 34 Firefox 29 Opera 19 IE9 Android 4
Полная загрузка 64,4 237,6 74,2 436,8 1714,6
Половина загрузки 51,6 142,8 65,4 358,6 1412,4

Стилевая диета

Тест позволил получить довольно интересные показатели. Например, Firefox выполнил этот тест в 1,7 раз медленнее чем тест с самым медленным селектором (тест 6). Android 4.3 потратил в 1,2 больше времени чем на тест с самым медленным селектором (тест 6). Internet Explorer выполнил этот тест в целых 2,5 раза медленнее чем тест с самым медленным селектором!

Как видите, все показатели для Firefox существенно уменьшились, когда была удалена половина стилей (примерно 1500 строчек). Устройство с Android также практически приблизилось к скорости теста с самым медленным селектором.

Удаление неиспользуемых стилей

Этот ужасный сценарий кажется вам знакомым? Огромные CSS-файлы со всевозможными селекторами (часто с теми, которые вообще нигде не применяются), масса нереально специфичных селекторов на 7 и больше уровней глубиной, ненужные префиксы, наобум натыканные идентификаторы и файлы размером 50-80Кб (и больше).

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

Этот первый шаг принесет больше пользы, чем тщательный выбор используемых селекторов. Пользователю придется скачивать меньше кода, браузеру — меньше обрабатывать — прирост скорости гарантирован в любом случае.

Впрочем, это не повлияет на реальную производительность вашего CSS.

Производительность внутри скобок

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

.link {
    background-color: red;
    border-radius: 5px;
    padding: 3px;
    box-shadow: 0 5px 5px #000;
    -webkit-transform: rotate(10deg);
    -moz-transform: rotate(10deg);
    -ms-transform: rotate(10deg);
    transform: rotate(10deg);
    display: block;
}

При применении такого правила были получены следующие результаты:

Тест Chrome 34 Firefox 29 Opera 19 IE9 Android 4
Дорогие стили 65,2 151,4 65,2 259,2 1923

Здесь хотя бы все браузеры достигли скорости как в тесте с самым медленным селектором (у IE скорость была в 1,5 раза меньше чем в тесте с самым медленным селектором (10) и устройство с Android справилось в 1,3 раза медленнее, чем с самым медленным селектором (тест 6)), однако это ещё не полная картина. Попробуйте прокрутить страницу! Перерисовка страницы с такими стилями заставит ваш компьютер рыдать.

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

При проведении теста с тяжелыми стилями на MacBook Pro 15" с ретиной, время рендеринга в режиме постоянной отрисовки в Chrome не опускалось ниже 280мс (помните, мы стремимся к 16 мс и ниже). Для сравнения, для первой тестовой страницы с селекторами оно не поднималось выше 2,5 мс. И это не опечатка. Эти свойства привели к 112-кратному возрастанию времени отрисовки. Вот такие вот увесистые свойства.

Какие свойства являются тяжелыми?

«Тяжелая» пара свойство/значение — это та, которая практически наверняка создаст для браузера дополнительную нагрузку, когда ему придётся перерисовать страницу (например, при прокрутке).

Как знать какой стиль окажется «тяжелым»? К счастью, в этом случае можно применить здравый смысл и довольно точно догадаться что приведёт к дополнительной нагрузке на браузер. Всё, что предполагает выполнение изменений/рассчётов перед отрисовкой страницы, будет трудозатратным для браузера. Например, box-shadow, border-radius, прозрачность (так как браузеру нужно рассчитать, что должно быть отображено под ней), трансформации и убийцы производительности — CSS-фильтры; если производительность является для вас приоритетом, все свойства вроде этих — ваши злейшие враги.

Ещё в 2012 году Юрий Зайцев под ником «kangax» написал замечательный пост в блоге также посвящённый производительности CSS. Он использовал различные инструменты разработчика для измерения производительности. Он провёл отличную работу и показал как различные свойства по-разному влияют на производительность. Если вас интересует подобная информация, вам стоит прочитать этот пост.

Заключение

Вот мои выводы из этой небольшой истории: корпеть над селекторами с целью повышения производительности в современных браузерах — бессмысленно; большинство методов выбора теперь работает так быстро, что на это не стоит тратить время. Более того, у браузеров не прослеживается единое мнение о том, какие селекторы являются медленными. Когда вам нужно ускорить CSS, селекторы — это то, на что следует обратить внимание в последнюю очередь. Чрезмерное количество неиспользуемых стилей, скорее всего, обойдётся вам дороже в плане производительности, чем любые селекторы. 3000 строчек лишнего кода на странице — не такое уж редкое явление. Хотя часто принято сваливать все стили в один громадный styles.css, если для разных разделов вашего сайта/веб-приложения можно добавить разные (дополнительные) таблицы стилей (в стиле графа зависимостей), это может стать лучшим решением. Если в течение времени в ваш CSS вносили изменения несколько разработчиков, обратитесь к инструментам вроде UnCSS для автоматического удаления стилей — делать это вручную сомнительное удовольствие! Битва за высокопроизводительный CSS не будет выиграна за счёт используемых селекторов, она будет выиграна с помощью рассудительного использования свойств и значений. Быстрая отрисовка чего-либо на странице, конечно, важна, но не менее важно то, насколько быстрой кажется работа страницы при взаимодействии с пользователем. В первую очередь обратите внимание на пары тяжелых свойств и значений (здесь вам пригодится режим постоянной отрисовки в Chrome), оптимизация этой части кода, скорее всего, принесёт наиболее ощутимый результат.

Логотип компании «Одноклассники»

Статья переведена благодаря спонсорской поддержке компании «Одноклассники».