Getting Around Internet Explorer’s 4,096 CSS Rule Limit… Again

In 2012, I wrote about handling IE8’s limit of 4096 selectors in any single CSS file. It’s been several years since then… and this is still a problem.

Using SCSS and frameworks makes it distressingly easy to hit these limits. In a perfect world, we’d be able to avoid them altogether, as the bloat is not really a positive thing. However, until I’m able to refactor things to fit under such limits (or until such limits are lifted and IE9 is firmly behind us), here’s another solution.

This time my solution is a little more polished. My current application is built on Ember and uses the Ember-CLI tool for managing its build process. Ember-CLI in turn uses Broccoli, which is a very flexible asset compiliation tool. I was able to use a few Broccoli module to handle splitting files automatically.

If you’re using straight-up CSS, you should be able to just drop the Ember CLI CSSSplit plugin into your pipeline and be done with things. But at the time it looked like the Ember CLI plugin assumed CSS and would not find my source files if I used SCSS, so I needed to drop down a level in the toolchain.

Broccoli is a very small build system that is based on the concept of trees: Each Broccoli plugin takes as input a tree of files/directories and returns as its output a new tree of files/directories. This forms a pipeline and lets you perform some very powerful transforms on your applications’ files.

The Broccoli pipeline I ended up building looked like:

  1. Use Ember CLI to compile the app and produce an output tree without asset fingerprinting.
  2. Use funnel to split the tree into two: CSS & HTML (to be split), and everything else (to be copied without modification).
  3. Run Broccoli CSSSplit to filter the large CSS files.
  4. Fingerprint the files.
  5. Then merge our postprocessed tree back into all our other code.

Here’s what my Brocfile looks like in source code:

var csss = require('broccoli-csssplit');
var assetRev = require('broccoli-asset-rev');
var funnel = require('broccoli-funnel');
var mergeTrees = require('broccoli-merge-trees');

var appTree = app.toTree();

// This section splits any CSS files to be below the IE9 limit of 4096 selectors.
//
// It is slow, so we don't do it in dev.
if(app.env != "development") {
  // In addition to splitting the CSS files, we also include the HTML in the
  // tree too. CSSSplit will ignore them, and then after we apply the MD5
  // fingerprinting, it will have the index html file so that it can rewrite
  // the asset links so they include the new split files.
  //
  // We do this twice to create two non-overlapping trees. We *could* just use
  // {overwrite:true} when we merge, but this way it is explicit in code which
  // is postprocessed.
  var cssTree = funnel(appTree, {
    include: [ "**/*.css", "**/*.html" ]
  });
  appTree = funnel(appTree, {
    exclude: [ "**/*.css", "**/*.html" ]
  });

  var splitTree = csss(cssTree);
  splitTree = assetRev(splitTree, {
    replaceExtensions: ['html']
  });

  appTree = mergeTrees([appTree, splitTree]);
}

module.exports = appTree;

This pipeline ends up handling most of the work. I still wrote a simple integration test that hits the page, counts the selectors, and makes sure that each of the split files shows up in the DOM, but at least this time around most of the work of detecting and dealing with the limit is automated.