Simple 4096 selector test for IE

The point of this test is to easily see how the famed 4096 selector limit really effects your web apps.

There are three parts, the Sass file that will generate the number of selectors needed, the base HTML need to view this in the browser and then run the simple Python server to view this locally.

We have all heard about this, but the use case is confusing and what does IE really consider a 'selector' anyway? In this exercise I confirmed a few suspicions.

How does this test work?

It's pretty simple really. I have a Sass loop that will create a series of nested selectors to render text the color orange. What I did was that the HTML does not represent this specificity, so unless the 4096th rule is read by the browser, the test will have NO style.

// adjust number to set limit to loop of created selectors
$rules: 4095

// loop that will produce all the selectors so you don't have to
@for $i from 1 through $rules
  .block
    .foo
      .text-color
        color: orange

// The final selector that IE8 WILL NOT SEE
.text-color
  color: red
  font-size: 3em
  font-weight: bold

Then the HTML looks like:

<!doctype html>
<html class="no-js" lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <link rel="stylesheet" href="app.css">
  </head>
  <body>
    <p class="text-color">Hello world! Simple IE selector limit test scenario</p>
  </body>
</html>

I did this for the sake of NOT KILLING THE BROWSER! Have you run a test where there are 4095 rules being set for a DOM element and you are looking for the 4096th? Even Chrome chokes a little. IE8 just has a shit fit if you try to open that shitty inspector.

With this test, either the browser will apply the last selector and apply the style, or it won't. Pretty simple.

4095 is REALLY the limit

In running this test I now know for sure that the limit of read selectors really is 4095, the 4096th will be ignored. Pretty simple.

Easy test for selector count

Okize's selector-detector is a npm tool you can install locally. It does require that you are running the CSS from a server, so running the Python Simple Server works really well.

Run the server

$ python -m SimpleHTTPServer 3000

Run the test

selector-detector http://localhost:3000

Get this

Retrieving CSS data from http://localhost:3000
{
  "page": "http://localhost:3000",
  "result": [
    {
      "name": "app.css",
      "url": "http://localhost:3000/app.css",
      "type": "linked",
      "rules": 4096,
      "selectors": 4096,
      "declarations": 4098
    }
  ]
}

Nested selectors do not count

Do nested selectors count? No, not from what I can tell.

.block .foo .text-color {
  color: orange;
}

and

.text-color {
  color: red;
  font-size: 10em;
  font-weight: bold;
}

equal TWO selectors. I suspected that compound selectors were counted as one, but now I have that proof. Cool.

Basically, anything up to and including the {} is considered a selector. And selector count IS NOT the count of unique selectors, although I wish we could build software to be this smart o_O

So what would be the selector count for the following?

.block .foo {
  width: 100%; }

.block .foo .text-color:after {
  color: orange; }

.text-color {
  color: red;
  font-size: 10em;
  font-weight: bold; }

The selectors of .block, .foo and text-color are being reused in the three rules, but since each rule is a single selector, the total from this is 3.

{
  "page": "http://localhost:3000",
  "result": [
    {
      "name": "app.css",
      "url": "http://localhost:3000/app.css",
      "type": "linked",
      "rules": 3,
      "selectors": 3,
      "declarations": 5
    }
  ]
}

Performance: Sass, Extends and OOCSS

I have been a lover and heavy user of Sass's @extend directive directive, but I am also very aware of the issues that this may cause when used incorrectly. If you are not paying attention then misuse of the @extend directive can produce a large number of needed selectors.

We as performance minded developers got into the mind wrap that reusing CSS rules was literally the worst thing we could ever do. While I agree that egregious over-duplication of CSS rules was not really helping anyone, I think that for most that the pendulum swung too far. Take the following code for example:

$selectors: Meggings freegan polaroid keytar hashtag swag ugh Portland gluten-free kitsch mixtape Neutra Tote bag fixie dreamcatcher locavore Banksy quinoa fashion axe Photo booth Echo Park before they sold out cold-pressed scenester farm-to-table migas Fap four loko Tumblr gastropub twee PBRB kale chips Actually Kickstarter Portland four dollar toast butcher bicycle rights plaid typewriter sustainable PBRB retro pug swag Banh mi cliche Carles leggings pork belly butcher Intelligentsia Roof party salvia heirloom tote bag Tumblr synth

%default-block-reset
  float: left
  width: 100%

@each $selector in $selectors
  .#{$selector}
    @extend %default-block-reset

As you can imagine, this will puke out a pretty nasty chain of selectors just to support these two CSS declarations.

{
  "page": "http://localhost:3000",
  "result": [
    {
      "name": "app.css",
      "url": "http://localhost:3000/app.css",
      "type": "linked",
      "rules": 1,
      "selectors": 75,
      "declarations": 2
    }
  ]
}

And converting this to a mixin versus an extended selector doesn't make things any better.

{
  "page": "http://localhost:3001",
  "result": [
    {
      "name": "app.css",
      "url": "http://localhost:3001/app.css",
      "type": "linked",
      "rules": 75,
      "selectors": 75,
      "declarations": 150
    }
  ]
}

Aside from the increased number of rules and declarations that the browser has to go through, there are no less selectors. One can easily argue that repeating declarations has an impact on performance. Different CSS techniques and their performance is a great article and performance test that walks through these performance differences and the data is pretty interesting.

How do I solve this problem? I use all three techniques. Extending placeholder selectors IS HUGE and I am not walking away from that. The key to either writing placeholders, OOCSS or mixins is that you create these selectors with intent and be specific about their responsibility.

The pragmatic part of me depends on a refactoring approach. Build your component, write your rules, selectors and declarations. It's when you start to see patterns emerge that you can begin to refactor and optimize for ease of maintenance and performance. While they have independent concerns, they are not mutually exclusive.

It's things like clearfix that get us into trouble. Is it better to make this a placeholder selector or place a class in the DOM? Each have their +/- and frankly it just sucks that we have to do these things. We can easily say that we shouldn't make this a mixin.

With the increased adoption of flex-box and 100% support for inline-block we can really get away from using floats for everything in our layouts despite a few hacks.

What about IE emulators?

They don't work. What I discovered running Modern.IE VMs is that when running IE11 in IE8 emulation mode, the limit was never an issue. The test came back with the 4096th selector being read.

When running the same test on a Windows XP VM with native IE8 I saw the expected result, the 4096th selector was not read.

Conclusion

This simple test shows you how this works. To test you need to use IE8 natively, not an emulator. Run your code through this selector tester and for the LOVE OF PETE, stop writing so many damn selectors!!!!

Interesting selector tests