Почему браузер читает CSS-селекторы справа налево

Если вы не знали об этом, браузер читает селекторы справа налево, то есть обрабатывая селектор #widget .heading span он в первую очередь найдёт все спаны, затем выберет только те, которые лежат внутри .heading, а затем оставит только спаны, лежашие внутри #widget.

Звучит нелогично, но причина такого поведения в том, что задачи разработчика и браузера несколько отличаются. Задача разработчика при чтении или составлении селектора — получение соответствующего ему набора элементов. А для понимания задачи браузера нужно немного контекста.

Прежде чем отрисовать страницу, браузер строит дерево отображения (render tree). Оно состоит из объектов отображения — визуальных элементов страницы, расположенных в том же порядке, в котором они должны быть выведены на страницу. Дерево отображения строится на основе DOM, но в него не попадают невизуальные элементы вроде head или элементы с display: none;, а сложным элементам вроде select могут соответствовать несколько объектов отображения.

Отрисовка страницы состоит из нескольких этапов, но нас интересует один — компоновка объектов отображения. На этом этапе браузер проходит по дереву отображения и рассчитывает размеры и положение каждого объекта на странице. Чтобы посчитать для объекта отображения эти значения, браузеру нужно знать стили, которые применены к этому объекту. Для этого ему нужно найти в таблице стилей все блоки правил, применяемые к элементу, соответствующему объекту отображения. И здесь мы подошли к ответу на исходный вопрос.

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

Рассмотрим тот же пример с селектором #widget .heading span. Предположим, что браузеру нужно определить, подходит ли этот селектор к элементу div. Если бы браузер читал селектор слева направо, ему бы пришлось найти на странице элемент с идентификатором widget, внутри него найти все элементы с классом .heading, а затем найти внутри все span и проверить, есть ли среди них тот самый div. Это очень много работы. А благодаря чтению справа налево браузер может сразу определить, что span и div — принципиально разные элементы и поэтому селектор не подходит.