Why we don't have a parent selector

On a seemingly regular basis, I see this discussion come up as to whether CSS should have a particular feature like the parent selector and while I haven't worked on a browser engine, I have my theories.

In short: performance.

How CSS gets evaluated

With work, I've had to do quite a bit of examination of performance. We run a number of tools over an application to determine where the bottlenecks are. One such application is Google Page Speed which provides a number of recommendations to improve JavaScript and rendering performance. Before I get into its recommendations, we need to understand a little better about how browsers evaulate CSS.

The style of an element is evaluated on element creation

We often think of our pages as these full and complete documents full of elements and content. However, browsers are designed to handle documents like a stream. They begin to receive the document from the server and can render the document before it has completely downloaded. Each node is evaluated and rendered to the viewport as it is received.

Take a look at the body of an example document:

<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>

The browser starts at the top and sees a body element. At this point, it thinks it's empty. It hasn't evaluated anything else. The browser will determine what the computed styles are and apply them to the element. What is the font, the color, the line height? After it figures this out, it paints it to the screen.

Next, it sees a div element with an ID of content. Again, at this point, it thinks it's empty. It hasn't evaluated anything else. The browser figures out the styles and then the div gets painted. The browser will determine if it needs to repaint the body—did the element get wider or taller? (I suspect there are other considerations but width and height changes are the most common effects child elements have on their parents.)

This process continues on until it reaches the end of the document.

Here is a visualization of the reflow/repaint process in Firefox:

CSS gets evaluated from right to left.

To determine whether a CSS rule applies to a particular element, it starts from the right of the rule and works it's way left.

If you have a rule like body div#content p { color: #003366; } then for every element—as it gets rendered to the page—it'll first ask if it's a paragraph element. If it is, it'll work its way up the DOM and ask if it's a div with an ID of content. If it finds what it's looking for, it'll continue its way up the DOM until it reaches the body .

By working right to left, the browser can determine whether a rule applies to this particular element that it is trying to paint to the viewport much faster. To determine which rule is more or less performant, you need to figure out how many nodes need to be evaluated to determine whether a style can be applied to an element.

Rules

Going back to Page Speed, let's take a look at a couple of the recommendations it provides:

Avoid descendant selectors

Avoid child or adjacent selectors

Of course, ID selectors are the fastest. A rule of #content can be evaluated quite quickly for an element being processed. Does it have the ID or not? Class selectors are almost as fast. There are no other dependencies that need to be checked.

Descendant selectors like .content .sidebar are problematic because to determine whether it should apply the styles to .sidebar , it has to find .content . Child selectors (Ex. .content > .sidebar ) are better than general descendant selectors because the browser only has to check one other element instead of mutiple elements.

Universal and Tag Selectors

Within its recommendations for descendant, child or adjacent selectors, it says to avoid the universal and tag selectors.

Take a look at the following example:

#content * { color: #039; }

With the ID selector there, your initial thought might be that this is really fast. The problem is that with the browser engine evaluating from right to left, the universal selector matches first. For the browser to detemine whether this element should be this deep shade of blue, it now has to check every ancestor element until it finds an element with an ID of content .

And it'll have to do this for every single element on the page.

Now that we understand when an element gets evaluated, how the selectors are determined, and how it might impact performance, let's look at an example issue.

Why IE took so long to get :last-child support

A common complaint: most browsers these days have support for :last-child —except Internet Explorer. (It'll be in IE9!) One might think, "how much harder can :last-child be if you have :first-child ?"

Let's pretend we're a browser and we're parsing that document example I used earlier.

/* The CSS */ .module > p:first-child { color: red; } /* first rule */ .module > p:last-child { color: blue; } /* second rule */

As we go into the first div , we see that we have a paragraph. The browser sees something like this:

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

Should the first rule be applied to this paragraph? Yes, it's a paragraph; yes, it's the first child; and yes, it's the direct child element of an element with the class module.

Should the second rule be applied to this paragraph? It is currently the last element. But we haven't loaded any more elements in, so we're just not sure.

Regardless of how I handle this dilemma, we now have the problem of having to re-evaluate the styles for two elements for every new element that we add to the DOM. If we add another paragraph after the first, then we have to re-determine what styles should be applied to the one previous to that.

How do browsers actually handle this?

I wasn't entirely sure how each browser would handle parsing :last-child, so I put together some test cases:

The first example should be very unexciting. In every browser, including IE9, things load up and appear correctly. Within the div , the first element is red and the last is blue. Take a look at the second example, however, and you'll see some interesting behavioural differences between the browsers.

The second example pauses before and after every paragraph in the div .

In Firefox, the first paragraph is initially rendered blue. When it loads the second paragraph, the first turns to red and the second paragraph is blue. Finally, when the third paragraph is loaded, the second one turns to the browser default and the third one turns to blue. Firefox treats the last element loaded as the last element until it gets a new one.

In Safari, Chrome and Opera, we see something slightly different. The first paragraph is red. The second one is rendered black. The last paragraph is rendered black until the closing div tag is received a second later. At which point, the last paragraph turns blue. These browsers don't treat any element as the last element until it closes the parent element.

In Internet Explorer 9 beta, I've discovered an interesting bug. While the static page loads correctly, the forced-pause version ends up with an interesting side effect. The first paragraph is blue, then the second paragraph is blue and then the third. Once the closing div is loaded, the second to last paragraph is changed to default black. IE9 is trying to behave like Webkit and Opera but...well...fails. Time to file a bug report with Microsoft.

Why don't we have a parent selector?

That was a fair amount of explanation to get to the original question. The problem isn't that we couldn't have a parent selector. The problem is that it would introduce a performance concern when it comes to determining what CSS rules apply to a given element. If Google Page Speed doesn't like universal selectors then you can guarantee that a parent selector would end up at the top of the list, far exceeding any performance issues you might have with universal selectors.

Let's take a look at why. First off, let's come up with an example syntax for our parent selector.

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

The problem is that we can't evaluate this rule until either we match the criteria or until all elements within the node are loaded. Related to that, we have to evaluate this and all other rules (in case of specificity issues) that apply to this element for each descendant element that we load.

Looking at a chunk of our document:

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

Based on what we've seen, the module would get rendered without the styles from the parent selector coming into play. When the first p element gets loaded, it'll have to re-evaluate to see if the parent selector applies to the div . It'll do this again for the next paragraph. And again for the third. Finally, when it loads the span, the parent selector will apply to the parent div and the element will get re-rendered.

Now what? If it changes any inheritable CSS properties, every single child element needs to be re-evaluated and re-rendered. Ouch.

Why can JavaScript solve this problem but CSS can't?

It's a bit of an illusion that JavaScript solves the problem. Generally speaking, JavaScript polyfills or regressive enhancements (or whatever you kids call it these days) only run once after the DOM has completely loaded.

To truly behave the way CSS does, any script that solves these problems would have to run after every single element was rendered to the page to determine whether a polyfill would need to be applied. Remember CSS expressions in Internet Explorer? There's a reason why they were a performance issue.

Not Impossible

Will we ever get a parent selector? Maybe. What I've described is not technically impossible. In fact, quite the opposite. It just means that we'd have to deal with the performance implications of using such a feature.