Почему у нас нет селектора по родителю

Джонатан Снук  20 сентября 2011

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

Вкратце: производительность.

Как работает CSS

В связи с моей работой я делаю много тестов производительности. Для определения «узких мест» мы используем массу приложений. Например, Google Page Speed, который дает рекомендации по улучшению производительности JavaScript и рендеринга. Прежде чем я перейду к рассмотрению этих рекомендаций, нам нужно разобраться как браузеры работают с CSS

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

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

Взгляните на этот документ:

	<body>
	    <div id="content">
	        <div class="module intro">
	            <p>Lorem Ipsum</p>
	        </div>
	        <div class="module">
	            <p>Lorem Ipsum</p>
	            <p>Lorem Ipsum</p>
	            <p>Lorem Ipsum <span>Test</span></p>
	        </div>
	    </div>
	</body>

Браузер начинает сначала и видит элемент <body>. В этот момент времени считается, что этот узел не имеет дочерних узлов. Ничего более не рассматривается. Браузер определяет соответствующий ему обобщенный набор стилей и применяет его к элементу. Какой шрифт, цвет, интерлиньяж? После того, как это все будет выяснено, элемент отображается на экране.

Далее браузер видит элемент <div> со значением атрибута ID content. И снова в этот момент времени браузер считает его пустым. Он не рассматривает другие элементы. Как только браузер рассчитает стиль, элемент отображается на экране. Затем браузер определяет нужно ли перерисовать <body> — стал ли элемент шире или выше? Я подозреваю, что там есть масса других проверок, но изменение ширины и высоты — самый распространенный способ повлиять на отображение родительского узла.

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

Вот как выглядит визуализация процессов перерисовки в Firefox:

CSS селекторы анализируется справа налево

Чтобы определить, применяется ли CSS-правило к определенному элементу, браузер рассматривает селектор справа налево.

Если у вас есть селектор body div#content p { color: #003366; }, то, когда каждый элемент появляется на странице, браузер проверяет, является ли он параграфом. Если да, он начинает подниматься вверх по DOM и ищет <div> со значением атрибута ID равным content. Если он его находит, то продолжает подниматься по DOM пока не найдет <body>.

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

Правила

Возвращаясь к Page Speed, давайте рассмотрим несколько его рекомендаций:

  • Избегайте селектора по потомку: .content .sidebar;
  • Избегайте селектора по дочернему элементу: .content > .sidebar и селектора по следующему элементу: .content + .sidebar.

Конечно, селекторы по ID — самые быстрые. Проверить применимость селектора #content к рассматриваемому элементу можно очень быстро. Есть у него этот ID или нет? Селекторы по классу практически такие же быстрые, так как нет никаких связанных элементов, которые надо проверять.

Селекторы по потомкам, такие как .content .sidebar — более ресурсоемкие, так как, чтобы определить надо ли применять правило к .sidebar, браузер должен найти .content. Cелектор по дочернему элементу, например, .content > .sidebar, лучше селектора по потомку, так как браузер должен проверить только один элемент вместо множества.

К сожалению, селекторы + и > не поддерживаются IE6. Так что если его поддержка актуальна для вас, то про них придется забыть. Примечание переводчика.

Селектор по тегу и универсальный селектор

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

Рассмотрим следующий пример:

#content * { color: #039; }

Так как в селекторе присутствует ID, то можно подумать, что этот селектор обрабатывается очень быстро. Проблема в том, что браузер обрабатывает селектор справа налево и сперва проверяется универсальный селектор. Для того чтобы браузер мог определить, должен ли цвет текста элемента быть тёмно-синим, надо проверить каждый предок элемента, пока не будет найден предок с атрибутом ID равным content или не будет достигнут корень документа.

И это должно быть сделано для каждого элемента на странице.

Теперь, когда мы понимаем, как элемент обрабатывается, как определяется применимость правил к элементу, давайте рассмотрим пример.

Почему IE долго не поддерживал :last-child

Все жаловались: у всех браузеров, кроме IE, есть поддержка :last-child (она появилась только в IE9!) Некоторые могли подумать насколько же сложнее сделать :last-child, если уже реализован :first-child?

Давайте представим, что мы — браузер и мы парсим документ-пример, который я приводил ранее.

	.module > p:first-child { color: red; } /* Первое правило */
	.module > p:last-child { color: blue; } /* Второе правило */

Когда мы рассматриваем внутренности первого <div>, мы видим, что там есть параграф. Браузер видит что-то вроде этого:

	<div class="module">
	    <p>Lorem Ipsum</p>

Нужно ли применить первое правило к параграфу? Да, это параграф; да, это первый дочерний узел; и, да, это непосредственный потомок элемента с классом module.

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

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

Как на самом деле это делает браузер?

Я не мог сказать с абсолютной уверенностью, как браузеры парсят :last-child, так что я создал несколько тестов:

Первый пример весьма скучен. В любом браузере, включая IE9, всё отображается корректно. Внутри <div> первый элемент красный, а последний синий. Но посмотрите на второй пример, и вы увидите интересные отличия в поведении браузеров.

Второй пример приостанавливается перед добавлением каждого параграфа в <div>.

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

В Safari, Chrome и Opera мы увидим другой подход. Первый параграф красный. Второй отображается чёрным. Последний параграф отображается чёрным, пока браузер не получит закрывающий тег </div>. В этот момент последний параграф становится синим. Эти браузеры не рассматривают элемент как последний, пока не будет закрыт родительский.

В Internet Explorer 9 Beta я нашел интересный баг. В то время, как статическая страница отображается корректно, версия с паузами отрабатывает с любопытным побочным эффектом. Первый параграф синий, второй параграф синий и затем — третий. Когда закрывающий тег </div> загружен, предпоследний параграф меняет цвет на чёрный. IE9 пытается обрабатывать селектор как Webkit и Opera, но… м-м… не выходит. Надо бы отправить багрепорт в Microsoft.

Почему у нас нет селектора по родителю?

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

Давайте посмотрим почему. Первым делом давайте приведём пример синтаксиса для селектора по родителю.

div.module:has(span) { color: green; }

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

Посмотрите на часть нашего документа:

	<div class="module">
	    <p>Lorem Ipsum</p>
	    <p>Lorem Ipsum</p>
	    <p>Lorem Ipsum <span>Test</span></p>
	</div>

Исходя из того, что мы видим, .module будет отображён без использования правила, применяемого селектором по родителю. Когда будет загружен первый элемент <p>, нужно повторно оценить применимость селектора по родителю к <div>. Нужно сделать это снова для следующего параграфа. И снова, для следующего. Наконец, когда <span> загружен, селектор по родителю будет применен к родительскому <div>, и элемент нужно будет повторно перерисовать.

И что теперь? Теперь, если изменится любое наследуемое CSS-свойство, каждый потомок должен будет повторно анализироваться и перерисовываться. Ох…

Почему проблему можно решить с помощью JavaScript?

Это только кажется, что JavaScript решает проблему. В общем случае JavaScript-заплатки (заплатки — polyfills — части кода, обеспечивающие функциональность, которую должен обеспечивать браузер. Примечание переводчика). Или регрессивное усовершенствование (или как там вы, молодежь, это сейчас называете) запускаются только один раз, после полной загрузки DOM.

Для того чтобы действительно имитировать поведение CSS, любой скрипт, решающий эту проблему, должен запускаться после отображения каждого элемента на странице, чтобы определить, нужно ли применить нашу «заплатку». Помните CSS-expressions в Internet Explorer? Именно по этой причине они вызывали такие проблемы с производительностью.

Не невозможно

Появится ли когда-нибудь селектор по родителю? Возможно. То, что я опиcал, не невозможно. На самом деле — наоборот. Это значит только то, что нам придётся иметь дело с ухудшением производительности из-за использования этого селектора.

Перевод оригинальной заметки «Why we don't have a parent selector» Джонатана Снука (Jonathan Snook), опубликованной в блоге «Snook.ca».

Теги: ,

Комментарии +

  1. Роман Комаров 20 сентября 2011 в 17:11

    «Стиль элемента применяется в момент его создания»
    Но есть же :empty! Но есть же nth-last-child()!

    «CSS селекторы анализируется справа налево»
    В чём проблема анализировать из середины идя в обе стороны?

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

    Какой-нибудь `.b-block .b-block-element < .b-anotherblock` — не скажется на производительности так сильно как `li *`.

    Всё это спекуляции ленивых и трусливых разработчиков браузеров, вон, в вебките и имеющиеся CSS не реализован в полной мере: псевдоклассы да селекторы `~` или множественные `+` срабатывают только при загрузке. Да, фиксится только явным хаком, который таки заставляет вебкит запускать на нужных элементах постоянный ререндеринг.

    Если такой хак добавлять к `body` — то, конечно, мы получим тормоза. Но если применять точечно: никто ничего не заметит.

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

    В общем, я негодую :)

  2. Михаил 20 сентября 2011 в 18:06

    Если уйти от этого стиля "абы как, но хоть что-то" (чтобы браузеры выдавали exception при невалидности xhtml, как в случае с xml и тому подобное) и отрисовывать не постепенно, чанк за чанком, каждый раз переделывая то, на что только что потратили время, а загрузив до конца - то и этих проблем с производительностью не было.
    да, сначала надо будет подождать - но в чём проблема?

  3. Лев Солнцев 20 сентября 2011 в 18:29

    Роман, основной аргумент со стороны производителей браузеров: это усложнит обработку документа и применения CSS, причём с непредсказуемой сложностью. В отличие от твоих примеров, где, грубо говоря, достаточно проверить childNodes для empty или nextSibling для :last-child.

  4. kizu 20 сентября 2011 в 21:03

    А для :nth-last-child()-то?

    Чем сложнее пример, который я написал выше, чем какой-нибудь :nth-last-child(17n-11):hover *?

    Если так посмотреть, то надо вообще запретить создавать что-то новое: вон, большие внутренние тени и радиальные градиенты тормозят в вебкитах, в опере тормозит мой пример с трансформами, до недавнего время тупил позишн фиксед, а страница до сих пор хреново работает рефлоу при вертикальном ресайзе страницы. Ну а в фф ещё в третьей версии (не так давно) была неверная реализация селектора «+» — почему его не вычеркнули из стандартов? Вон, rowspan="0" же вычеркнули из спек, наверное, из-за того, что одно время только в фф оно работало, а остальным было «сложно»?

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

  5. SilentImp 20 сентября 2011 в 22:15

    Роман Комаров

    Но есть же :empty! Но есть же nth-last-child()!

    Есть. И в статье описано то, как они обрабатываються. На примере last-child(). А за одно какие проблемы с этим связаны и почему это более ресурсоемко, чем нам бы хотелось.

    В чём проблема анализировать из середины идя в обе стороны?

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

    почему бы не ограничить явно дальность лукапа, как с регекспами и лукбехайндом?

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

    Всё это спекуляции ленивых и трусливых разработчиков браузеров

    Ну вот, мы с вами вместе весело посмеялись и пошли читать новые интересные спеки.

  6. SilentImp 20 сентября 2011 в 23:14

    Трёхмерную графику рендерить мы можем, а на один уровень вниз посмотреть — нет.

    Я вот как раз сейчас с ней эксперементик ставлю. Лучше бы они сосредоточились на селекторах. То, что мы можем рендерить трехмерную графику это пока сильное преувеличение. И это когда речь о паре узлов идет. А если подумать про обраьботку всего dom сколько бы то ни было не тривиальную … мне уже грустно. И chrome на macbook pro с 8Gb ram и i5 тормозит…

  7. SilentImp 20 сентября 2011 в 23:18

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

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

  8. kizu 21 сентября 2011 в 0:57

    это более ресурсоемко, чем нам бы хотелось.

    и

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

    Тут вопрос в чём: или бояться что-то делать из-за каких-то причин (производительность? сложность в имплементации?), и делать то, что выгодно с точки зрения маркетинга (тот же 3D — я совершенно согласен с тем, что лучше бы они что-то другое делали) или же то, к чему подгоняет лень/желание создать спецификацию ради спецификации.

    Лейауты? практически всё, за редкими-редкими исключениями сейчас можно сделать всякими грязными хаками. Ну, т.е. ничего нового, чисто упор на семантику, упрощение разработки и т.д. Нет, я не против! Флексбокс ом ном ном, грид многообещающ и т.д. Но. Как раз с лейаутами и возникает, кхм, лицемерие? Уже с имеющимися лейаутами на флоатах/инлайн-блоках возникают проблемы с производительностью. Когда изменение в конце одного блока вызывает рефлоу всей страницы. Те же раскладки гридом-флексбоком-таблицами — ровно та же история, только всё ещё сложнее.

    И они говорят, что селекторы тормозят. Да всё тормозит если неправильно применять или имплементировать! Флоаты тормозят! Таблицы тормозят! Тени тормозят, градиенты тормозят, бордер-радиусы тормозят! Что же теперь, убиться?

    Почему для раскладок сделано исключение, а для селекторов — нет? Вот этот вопрос меня волнует.

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

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

    А вот раскладок — всё больше и больше видов. Инновационные разработки Adobe на движке вебкита с перетекающими блоками! Красота!

    А статья хорошая, я согласен :) Но вот вопрос про исключение остаётся.

  9. kizu 21 сентября 2011 в 1:02

    Ну т.е. ещё раз: статья ок, перевод ок, но у меня накипело, простите. И куда мне это писать, как не в статью с таким названием? Очень много было случаев, когда вот прямо была нужда применять такие селекторы (или другое, о чём я писал и что «вычеркнули»), и в каждом таком случае приходилось писать костыли, раздувать код, раздувать траффик пользователям, вместо того, чтобы сделать простое и элегантное решение. Так бы просто всё было! Эх.

  10. MT 21 сентября 2011 в 1:20

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

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

    В простейшем случае перед разбором целиком селектора браузер может быстро проверять наличие подстроки, соответствующей потенциально медленному его участку (например, ":has"), и в случае наличия таковой разбирать селектор не справа налево, а слева направо.

    P.S. И, безусловно, не параграф, а абзац, и не FireFox, а Firefox.

  11. Вадим Макеев 21 сентября 2011 в 1:27

    Марат, называть элемент <p> абзацем — это всё равно, что пытаться перевести на русский элемент <div>. Так уж сложилась профессиональное употребление. И это даже хорошо, поскольку не каждый HTML-параграф — это абзац, и наоборот. Firefox исправлен.

  12. MT 21 сентября 2011 в 1:32

    Вадим, из-за того, что где-то абзац ошибочно называют параграфом, абзац не перестаёт быть абзацем. Профессиональное употребление здесь ни при чём. Параграфы — у Лебедева. И в каждом параграфе — много абзацев. ;-)

  13. Вадим Макеев 21 сентября 2011 в 1:40

    Главный мой аргумент состоит в том, что я вижу разницу между <p> и блоком текста, отделённым красной или пустой строкой. Поэтому первый, только в контексте HTML-вёрстки, это параграф, а второй — абзац. Это как «киллер» и «убийца». Строго говоря, в отрыве от контекста, это одно и то же. Но вот только звучат они по-разому и обозначают схожие, но всё же отличающиеся явления.

  14. MT 21 сентября 2011 в 1:54

    Даже если на практике элемент P не всегда используется для разметки абзацев, это не делает его параграфом (иное, пожалуй, было бы софистикой). Если же хочется терминологической точности — имеет смысл использовать достаточно нейтральное «элемент P».

  15. SilentImp 21 сентября 2011 в 8:01

    MT, повторюсь … тут оторван от семантики. "Параграф" использован для однозначного определенения тега и получен путем транслитеративного перевода полного названия тега . Я не вкладываю в него значения, которое вкладывается в "параграф" или "абзац" с точки зрения типографики. Я озвучиваю название тега. И если бы я написал "абзац", то прямая связаь с p улавливалась бы на мой взгляд слабо. «элемент P» — тоже ОК, хотя imo звучит хуже. Но точно не абзац. Не в данном контексте.

    Почему для раскладок сделано исключение, а для селекторов — нет?

    kizu, думаю это вопрос "трендовости" и востребованности. Причем скорее первого, чем второго. html5 и новые блестящие игрушки на слуху. Этим можно хвастаться. А Округлением в опере или селектором по родителю — нет. Причем, скажем честно, нехватка селектора по родителю портит нам настроение гораздо реже, чем поплывший лаяут и связанные с ним проблемы. Просто потому, что лаяут мы делаем в любом пакете верстки без исключения, а селектор по родителю ножен в достаточно специфических случаях. Как то так, думаю.

  16. Иван 22 сентября 2011 в 8:57

    has(чего-то там) это по потомку, а не по родителю.

  17. Лев Солнцев 4 октября 2011 в 14:49

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

    $E > F an E element parent of an E element

  18. exessqd 28 октября 2011 в 15:36

    Меня иногда посещают, да да, они, мысли.

    одна из них.

    Как раньше разработчики не хотели переучиваться с табличной верстки на дивную так сейчас не хотят переучиваться с списковой на модульную. История циклична.

    не в тему конечно, но форума у вас нет.

  19. sd 22 ноября 2011 в 23:02

    Почему тогда повсеместно используются не эффективные селекторы? Например майл.ру:

    
    #Stats td.frst div {
        background: url("http://otvet.mail.ru/img/ico_stats.gif") no-repeat scroll left top transparent;
       ...
    }
    
    

    Смысл забивать этим голову верстальщику, если на практике это не влияет на производительность?

  20. Вадим Макеев 22 ноября 2011 в 23:12

    sd, если верстальщики Mail.Ru не заботятся об эффективности своего кода, это не значит, что все остальные должны следовать их примеру.

  21. sd 23 ноября 2011 в 3:56

    web-standards.ru

    #menu LI * {
    

    opera.com

    #footer #wwwsearch div {
    

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

  22. exessqd 4 января 2012 в 9:02

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

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

    Но есть особые случаи, например, тяжеловесные JS приложения
    где браузеру приходится 100*googleplex раз перерисовывать страницу и в этом случае прирост производительности в 300 мс может ощутимо ускорить быстродействие приложения.

  23. Phil 3 апреля 2014 в 16:24

    Сравним 2 селектора

    div.module > span
    

    и

    div.module:has(span)
    

    В первом случае происходит следующее:
    когда каждый элемент появляется на странице, браузер проверяет, является ли он span'om. Если да, он поднимается вверх по DOM и проверяет, является ли предок = div.module.
    Если да, то применяются к span'y указанные стили.
    Перерисовка каждый раз для каждого нового span'a внутри div.module

    Теперь второй случай (возможный на мой взгляд):
    когда каждый элемент появляется на странице, браузер проверяет, является ли он span'om. Если да, он поднимается вверх по DOM и проверяет, является ли предок = div.module.
    Если да, то к div.module применяются указанные стили.
    Перерисовка каждый раз для каждого нового потомка внутри div.module, пока не будет встречен ПЕРВЫЙ span. После нет смысла, так как логически правило :HAS уже будет выполнено.

  24. Phil 3 апреля 2014 в 16:28

    К комментарию выше, во-втором случае:
    Перерисовка ОДИН раз для ПЕРВОГО span внутри div.module.

Разрешённые HTML-теги: <blockquote>, <a href="…">, <del>, <strong>, <b>, <em>, <i>.
Исходный код блочного уровня для лучшего отображения обрамляйте в элемент <source>.

Перейти к началу