Frontender Magazine

Как писать эффективные с точки зрения производительности CSS-селекторы

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

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

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

CSS-селекторы

Для большинства из нас в CSS-селекторах нет ничего нового. Базовые селекторы — это тип (например, div), ID (например, #header) и класс (например, .tweet).

Менее распространенные селекторы включают базовые псевдоклассы (такие как :hover), селекторы CSS3 и селекторы на основе регулярных выражений, такие как :first-child и [class^="grid-"].

У селекторов есть встроенное поведение относительно производительности, и, ссылаясь на Стива Саудерса (Steve Souders), отсортировать CSS-селекторы от более к менее производительным можно, например, так:

Цитата из книги «Как сделать сайты еще быстрее» (Even Faster Websites) Стива Саудерса

Важно отметить, хотя селекторы по ID технически быстрее, с точки зрения производительности, разница очень невелика. С помощью CSS Test Creator Стива Саудерса можно видеть, что у селекторов по ID и селекторов по классу, в действительности, очень незначительная разница во времени перерисовки страницы.

В Firefox 6 под Windows я получил среднее время перерисовки страницы с простым селектором по классу в 10.9 мс. То же самое, но с селектором по ID, дало мне 12.5 мс, так что, в дейтвительности, с ID страница перерисовывалась медленнее, чем с классом.

Разницы в скорости между селектором по ID и по классу практически не существует.

Однако, тест селектора по типу (<a>), в отличие от селекторов по классу или ID, выдал гораздо большее время перерисовки страницы.

Тест на серьезно перенасыщенном селекторе с вложенными элементами дал около 440 мс!

Становится очевидным, что разница между селекторами по ID/классам и типами/вложенным элементам достаточно велика… Разницой же между ними самими можно пренебречь.

Обратите внимание: Эти цифры могут значительно отличаться на разных компьютерах и в разных браузерах. Настоятельно рекомендую протестировать ваш CSS самостоятельно.

Совмещаем селекторы

У вас могут быть независимые селекторы (например, #nav), которые выберут любой элемент с ID ‘nav’, или вы можете поставить в свой документ комбинированные селекторы, например, #nav a, который выберет все ссылки внутри любого элемента с ID ‘nav’.

Итак, мы читаем их слева направо. Мы видим, что мы сперва ищем #nav, а потом — все элементы внутри него. Браузеры читают селекторы наоборот: справа налево.

Там, где мы видим #nav c a внутри, браузеры видят a внутри #nav. Эта тонкая разница имеет огромное влияние на производительность селекторов, и ее просто необходимо знать.

Чтобы докопаться до сути почему происходит именно так, посмотрите вот это обсуждение на Stack Overflow.

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

Это имеет большое влияние на производительность CSS-селекторов...

Ключевой селектор

Ключевой селектор, как говорилось выше, это крайняя правая часть CSS-селектора. Это то, что в первую очередь видит браузер.

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

Такой ключевой селектор...

#content .intro{}

...вероятно, достаточно производителен, поскольку классы сами по себе являются производительным селектором. Браузер найдет все элементы .intro (которых, скорее всего, будет немного), а потом пройдет вверх по дереву DOM проверить, находится ли этот ключевой селектор внутри элемента c ID ‘content’.

Однако, именно такой селектор не силен в плане производительности:

#content *{}

Этот селектор делает вот что: смотрит на каждый элемент страницы (абсолютно каждый), а потом смотрит, находятся ли какие-нибудь из них внутри родительского элемента #content. Это очень непроизводительный селектор, поскольку его ключевой селектор является очень дорогим.

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

Предположим, что у нас есть огромная страница, она гигантских размеров, а еще у нашего сайта просто огромная посещаемость. На этой странице сотни или даже тысячи <a>. Кроме того, есть маленький раздел со ссылками на социальные сети внутри <ul> с ID #social. Скажем к примеру, что там идут ссылки на Twitter, Facebook, Dribbble и Google+. Итак, у нас на этой странице четыре ссылки на соцсети и сотни других ссылок.

В этом случае такой селектор будет неразумно дорог:

#social a{}

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

Для того чтобы побороть это - мы можем добавить более явный и специфический селектор, добавив класс .social-link к каждому из элементов <a> в области #social. Но это противочерит тому, что нам известно: мы ведь знаем, что не нужно добавлять лишние классы к элементам, когда мы можем использовать более чистую разметку с меньшим количеством кода.

Вот почему тема производительности кажется мне настолько интересной: это странный баланс между передовыми практиками веб-стандартов и просто скоростью.

Итак, если обычно мы имели:

<ul id="social">
    <li><a href="#" class="twitter">Twitter</a></li>
    <li><a href="#" class="facebook">Facebook</a></li>
    <li><a href="#"class="dribble">Dribbble</a></li>
    <li><a href="#" class="gplus">Google+</a></li>
</ul>

с таким CSS:

#social a{}

То теперь мы напишем:

<ul id="social">
    <li><a href="#" class="social-link twitter">Twitter</a></li>
    <li><a href="#" class="social-link facebook">Facebook</a></li>
    <li><a href="#" class="social-link dribble">Dribbble</a></li>
    <li><a href="#" class="social-link gplus">Google+</a></li>
</ul>

С таким CSS:

#social .social-link{}

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

И на самом деле мы можем сократить этот селектор еще больше, чтобы он не был перенасыщенным, вплоть до .social-link{}. Об этом читайте уже в следующем разделе…

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

Перенасыщенные селекторы

Итак, теперь мы знаем, что такое ключевой селектор, и что отсюда идет бóльшая часть работы браузера. Теперь мы можем оптимизировать дальше. Самое лучшее в хороших явных ключевых селекторах — это то, что вы можете обойтись без перенасыщенных селекторов. Перенасыщенный селектор выглядит примерно так:

html body .wrapper #content a{}

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

#content a{}

И что?

Итак, первый по порядку селектор означает, что браузер должен найти все элементы, a затем проверить их наличие в элементе с ID content и так далее, и так далее, вплоть до элемента html. Браузер вынужден из-за этого делать слишком много проверок, которые в действительности не нужны. Зная это, мы можем привести более реалистичные примеры, как этот:

ul#nav li a{}

К такому виду:

#nav a{}

Мы знаем, что если a находится внутри li, то она должна находиться внутри #nav, так что мы сразу выбрасываем li из селектора. Далее, раз #nav — это ID, который существует на странице в единственном экземпляре, то тип элемента, к которому он применен, абсолютно неважен: значит, мы можем выбросить ul.

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

Насколько все это необходимо?

Короткий ответ: не очень.

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

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

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

Но

Но пока все остается по-прежнему, браузерам все равно приходится делать ту работу, о которой мы говорили, какими бы быстрыми они ни становились. Даже если вам не нужно (или вы просто не хотите) применять что-либо из описанного здесь, в любом случае это определенно следует знать. Помните, селекторы могут быть дорогими в плане производительности, и по крайней мере самых зубодробительных вам стоит по возможности избегать. То есть если внезапно вы поймали себя на том, что пишете что-то такое:

div:nth-of-type(3) ul:last-child li:nth-of-type(odd) *{ font-weight:bold }

То, наверное, вы делаете что-то неправильно.

Я сам все еще относительный новичок в вопросах производительности CSS-селекторов, так что если я что-то пропустил или вам есть что добавить, пожалуйста, пишите в комментариях!

Еще об эффективности CSS-селекторов с точки зрения производительности

Я очень рекомендую сайты и книги Стива Саудерса. Это практически единственная рекомендация для дальнейшего чтения, и она вам реально пригодится. Парень шарит в своей теме!

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Harry Roberts
Автор:
Harry Roberts
Сaйт:
http://csswizardry.com/
Twitter:
@csswizardry
GitHub:
csswizardry
Vlad Andersen

Комментарии (29 комментариев, если быть точным)

Автар пользователя
mistakster

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

Кстати, есть смысл использовать «перенасыщенные селекторы» в качестве альтернативы !important для повышения специфичности? Или всё же предпочтительней !important?

Автар пользователя
mistakster

Ещё поделюсь интересным на эту тему: «А вы задумывались какова будет производительность каскадных селекторов, которые не находят элементов на странице?»

Оказывается, не всегда браузеры эффективно выявляли неиспользуемые селекторы. Один из старейших браузеров (и это не IE) как раз отличился в этом.

Автар пользователя
SilentImp

Кстати, есть смысл использовать «перенасыщенные селекторы» в качестве альтернативы !important для повышения специфичности? Или всё же предпочтительней !important?

Однозначно создавать вес увеличением каскада. !important перебивает все и вся. Его перезаписать будет потом проблематично. Без другого !important и еще большего каскада.

Автар пользователя
rohozhnikoff

Причина необходимости использовать повышение веса селектора - неправильная архитектура. Любое временное решение должно оставаться временным, чтобы можно было явно сказать, что из этого нужно, а что "хак". Поэтому лучше использовать !important - он всегда выглядит одинаково, влияет только на конкретный стиль (а не на весь массив свойств у селектора), не нарушает принцип независимости блоков/элементов (за счет того что не ориентируется на то, чем вы повысили вес селектора). И плевать, что он перебивает все и вся. Если у вас возникнет желание перебить !important, то может стоит немного подумать и найти другое решение?

P.S. Все вышесказанное работает только на создаваемыми вами проектах. Если это чужой проект с нагромождением неструктурированных стилей и вам надо только внести изменения - не стесняйтесь, используйте хоть !important + повышение веса.

Автар пользователя
SilentImp

Причина необходимости использовать повышение веса селектора - неправильная архитектура.

С чего бы? Управление весом селектора это вполне легитимное средство для того, что бы перезаписать более общее правило более специфическим. Например:

b{font-family:sans-serif;} blockquote{font-family: serif;} blockquote b{font-family: serif;}

http://jsfiddle.net/ebRme/ — например

!important - он всегда выглядит одинаково, влияет только на конкретный стиль (а не на весь массив свойств у селектора), не нарушает принцип независимости блоков/элементов (за счет того что не ориентируется на то, чем вы повысили вес селектора).

Мурад, это ты так шутишь? Если ты не используешь полностью независимые блоки, исключив из простыни каскад, то ты почти наверняка что то поломаешь такой конструкцией, перезаписав селектор по соответствующему тегу или классу в другом блоке. А если используешь и каскада нет, то тебе и перезаписывать ничего не надо и нет нужды ни в наращивании веса, ни в !important.

Автар пользователя
rohozhnikoff

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

Сам по себе пример синтетический, обычно таких правил десятки и ты для каждого из них повысил вес. Маловероятно что у тебя будет потребность перебивать !important во второй раз, в случае одного правила, но более вероятно что ты получаешь маленькую головную боль каждый раз, когда приходится повышать вес и подсаживаться на эту сомнительную практику, минусы которой я описал выше.

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

Автар пользователя
roose

А как насчет #social > a{}

Автар пользователя
mistakster

Селектор прямого потомка > в CSS чуть хуже, чем простой селектор, и несравненно лучше, чем обычный каскад. Что будет стоять перед ним не имеет особого значения. Вот для JS-движков наличие #id в самом начале просто необходимо.

Автар пользователя
roose

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

Автар пользователя
mistakster

Совершенно верно. Все селекторы работают одинаково — справа налево. Лучше только потому, что делается проверка всего лишь одного предка.

Автар пользователя
SilentImp

Мурад, ну почему же «синтетический»? Ты такой код даже для этого сайта найдешь. Но да, я постарался выбрать максмально абстрактный пример. Общая идея — уточнение. На самом верхнем уровне селекторов мы задаем общие свойства и затем все больше их уточняем. Как дерево. Все параграфы body у нас с кеглем 12px. А вот в виджете .news-widget — 14px А в блоке .news-widget aside, который появился через 3 месяца — вообще 10px. И если ты для .news-widget задал !important, то такое уточнение будет работать, только если ты там тоже его задашь.

Абсолютно независимые блоки это строго говоря не проблема.

.news-widget{} .news-widget-article{} .news-widget-article-title{} .news-widget-article-description{}

Другой вопрос я считаю такой подход осмысленным только на проектах с очень большим DOM и только если важны милисекунды. И при этом эти человекочитаемые классы в процессе деплоя еще и заменяются 1-2-3 буквенными абстракциями.

Мурад, ты правда думаешь, что использование !important без серьезных оснований считатется дурным тоном просто так, на пустом месте, на протяжении десятка лет? Ты говоришь какие то пространные вещи, но вес имеют только реальные кейсы. Хочешь что бы я отнесся серьезно к этому — давай код. И да, я уже слышал про NDA. Но я и сам неоднократно имел с ним дело. Если захочешь дать кейс — дашь. Не нарушая NDA. Мы с тобой это оба понимаем. А пока, прости, звучит как total bullshit.

Автар пользователя
rohozhnikoff

Тоесть, ты хочешь сказать что у тебя есть записи: body {font-size: 12px} .news-widget {font-size: 14px} и ты зачем-то использовал !important?

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

Автар пользователя
mistakster

Вот, например, такой кейс:

a { color: green; } /* reset.css */

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

.widget__special-link { color: blue !important; }

или

```

widget .widget__special-link { color: blue; }

a.widget__special-link { color: blue; } /* ну, или ещё какие-нибудь комбинации каскадов */ ```

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

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

Автар пользователя
SilentImp

Мурад, ну все равно, так все равно. Но в том, что любой подход «правильный» я не согласен. Ты даешь плохие советы.

Автар пользователя
SilentImp

mistakster а ты уверен, что у тебя ни при каких обстоятельствах не появится ссылка с классом .widget__special-link которая при определенных обстоятельствах будет какого то третьего цвета? Я бы не стал зарекаться. Особенно если разработку ведет больше одного человека.

Автар пользователя
SilentImp

mistakster и в догонку … как ты ховер задавать будешь? Тоже c !important? Иначе ведь не получится: http://jsfiddle.net/xEbCV/ И про все остальные псевдоклассы подумай. Или про добавление иконок из того же http://fortawesome.github.com/Font-Awesome/ с цветом, не совпадающим с цветом текста.

Автар пользователя
mistakster

Антон, конечно нет. Тогда можно будет с чистой совестью сделать рефакторинг.

Автар пользователя
SilentImp

!important может сыграть с вами злую шутку, даже если кажется, что все продумано. Давайте приберегать его на крайний случай, когда наращивание веса перестает быть выходом. Или для чужого кода в котором нет возможности разбираться. Или в ряде случаев для стилизации контента который получаем со стороны и из которого по какой то причине не можем вычистить представление. В общем !important нужно использовать там, где другие подходы вызывают серьезные проблемы. Это конечно imo. Но то, что это считается непреложным правилом уже много лет позволяет мне думать, что я прав.

Автар пользователя
SilentImp

mistakster и зачем рефакторить каждый раз такой кусок кода, если можно сразу сделать future-proof? Что плохого в конструкции .news-widget a{}?

Автар пользователя
SilentImp

Ну или a.news-widget-link{} если угодно.

Автар пользователя
mikeyakimenko

mistaksterСегодня в 07:12 вы не правы.

Плохо .widget__special-link { color: blue !important; }

плохо #widget .widget__special-link { color: blue; }

a.widget__special-link { color: blue; } - хорошо.

Что касается статьи, как-то странновато. Написал Американец, и "Уау" Эеврика! Реально каскады, не так страшны как кажется, А если у вас каскадв ~20 уровней, то уже да, сыграет роль. И сверстать каскадами так что бы тормозило и пользователь ощущал это, как либо, это нужно быть настоящим бэтманом.

Автар пользователя
mikeyakimenko

P.S. Авторы проекта, сделайте пожалуйста ссылки на комментарии!

Автар пользователя
SilentImp

Cделаю.

Автар пользователя
n2j7

mistakster, вы ошибаетесь, у селектора класса вес выше чем у селектора тега

http://www.w3.org/TR/2009/CR-CSS2-20090908/cascade.html#specificity

http://jsfiddle.net/xEbCV/1/

Мурад, начиная со средних и особенно в больших по объему кода проектах !important является исключительной проблемой! Вы никогда не должны рассчитывать на то, что что-либо будет постоянным, даже наоборот, вы всегда должны думать о том, что то что вы делаете должно быть максимально гибким. Повышать вес каскадингом нужно если того требует бизнес задача (Пример: сделать все кнопки на панели фильтров - синими) или уточнением селектора(Было: .myclass стало tag.myclass) в других случаях. Опиши к каким проблемам архитектуры это может привести или приводит? Или что в в такой архитектуре "проблемного" и выходом является использование "!important"?

"Лень - это для плохих разработчиков, наверно ты хорош в чем то другом, а разработка не для тебя..."

Автар пользователя
AndreyKozlovTomsk

Вот тоже интересная статья по селекторам, с поддержкой браузеров.

http://only-css.com/blog/article/selektory-css3

И если хорошо ими манипулировать, то не обязательно так извращаться.

Автар пользователя
iamstarkov

извращаться

простите, но вы о чём?

Автар пользователя
AndreyKozlovTomsk

"Реалистичный" пример из статьи - ul#nav li a{}

Мдееее....

Селекторы без указания вложенности работают в 2-5 раз медленнее (зависит от железа), чем с указанной например ~ или + или >.

Автар пользователя
iamstarkov

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

Автар пользователя
alfalyxx

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