Even though at Bindle we limit our support to IE9, we still fear the obscure IE bug. It leads to an aversion to booting up a Windows 7 VM. jQuery + Compass have (greatly) reduced the worries over IE quirks, but from time to time I am still left shaking my head.

These days I find I can get away with deferring IE testing until towards the end of a release process, as most of the time all we need to do is double check things look and behave right. So imagine my surprise when seeing this while loading our latest release:

Bindle doesn't look very good when IE isn't interpreting all of the stylesheets.

As you can see the majority of our stylesheets are not being interpreted. It turns out the problem is due to some internal limitations in IE, versions up to 9. Such browsers allow:

  • up to 4095 selectors per stylesheet [this is the one that was causing us the problem].
  • up to 31 @imported sheets
  • @import nesting up to 4 levels deep.

Why in this day and age do we have such a limit, you ask? Eric explains:

The root of the limitations is that Internet Explorer uses a 32bit integer to identify, sort, and apply the cascading rules. The integer’s 32bits are split into five fields: four sheetIDs of 5 bits each, and one 12bit ruleID. The 5 bit sheetIDs results in the 31 @import limit, and the 12 bit ruleID results in the 4095 rules-per-sheet limitation.

Chris Wilson comments that the feature was implemented in IE3 (released 1996), when keeping the memory requirement per stylesheet rule was a little bit more important.

Dealing with the problem

The specific issue is that Bindle has more than just 4095 selectors. The solution is to split the stylesheet up (for IE only), and to serve up chunks of it selectively to IE. So you’ll see in our <head> tag, something like:

    <link href="/assets/screen.css?body=1" media="screen" rel="stylesheet" type="text/css" />
    <!--[if IE]><link href="/split_assets/screen-2.css" rel="stylesheet" type="text/css" media="screen"><![endif]-->

The standard sprocketed stylesheet and a second, IE only stylesheet (containing the remaining rules from our stylesheet that it ignores).

How to work producing this screen-2.css into our Rails 3.1 / git / heroku workflow? The nicest solution is Bless, a node.js project that splits CSS files via a command line program, even adding @import statements to your first file to ensure you don’t need to change your html code at all. However, this solution isn’t practical when you are deploying to heroku, with it’s read-only file system, and git push style deployment. (If you are deploying to your own server via capistrano, I would advise following this route).

So what is a poor heroku based developer to do? I found a simple little CSS splitter ruby class which I tried to work into the sprockets assets workflow, so that the CSS files would get split on rake assets:precompile; however (not being a sprockets expert) it seemed that turning one file into multiple wasn’t part of the basic sprockets plan. Another possibility would be to hook somewhere else into the assets:precompile task, however I need my code to run after assets:precompile (and thus the css files generated), and I don’t know how to ensure that [1].

UPDATE: Zweilove have released this class as a gem, and have a workaround for the multiple-files problem, so it’s now possible to integrate it into the asset workflow.

A solution: rack middleware

Although working harder through one of the solutions outlined above would probably be the ideal way to do it, I secretly didn’t really want to, as I was itching for a chance to implement a rack middleware. I had previously had to dig around in middleware when solving an extremely scary bug that I encountered. I was keen to have a go at one of my own. Here’s the end result:

require 'css_splitter'
class SplitAssetEndpoint
  def initialize(app, path)
    @app = app
    @path = path
  def call(env)
    if env['PATH_INFO'] =~ Regexp.new('^' + @path + '/([^/]*)\-(\d+).css')
      filename = File.join(::Rails.public_path, ::Rails.application.config.assets.prefix, $1 + '.css')
      css = CssSplitter.split_to_part(filename, $2.to_i)
      [200, {
        'Content-Type' => 'text/css',
        'Content-Size' => css.bytesize.to_s,
        'Cache-Control' => 'public, max-age=31536000'
      }, [css]]

Here’s the modifications I made to the css splitter library to allow me to extract a single part of the file. The middleware itself is pretty simple. We simply look at the incoming request, and if it’s path matches (/split_assets/ in our case), it parses out the part you want, pulls it out of the CSS asset, and serves it back to you. Otherwise it simply passes the request through to the remainder of the stack.

Doesn’t this use a lot of resources parse + split the CSS every time an IE user accesses the site? Well, no, thanks to the magic of HTTP caching. We simply make sure that we place the SplitAssetEndpoint behind the Cache, like so:

config.middleware.insert insert_after Rack::Cache, 'SplitAssetEndpoint', '/split_assets'

Problem solved!

  1. rake.enhance should work, but doesn’t seem to hook in on Heroku. I guess it’s the special way they run assets:precompile. I would welcome suggestions about how to do this in the comments []
Tom Coleman

Co-creator of bindle.me, searching for simplicity, quality and elegance in technology, products and code.

See all Tom Coleman's posts

3 Responses to “Splitting the Asset: destroying arcane IE bugs on the ruby rack.”

  1. Jakob Hilden says:

    That’s also a nice solution.

    In the meantime we have further developed the Asset Pipeline integration and released a gem for it: https://github.com/zweilove/css_splitter
    It still requires a little bit of manual setup, but besides I think it’s pretty good. Not sure what exactly the pros and cons are between the two soltions.

  2. [...] into 8 rules. A few of these in your CSS can quite easily put you over the limit. I’ve found more than one solution, but nothing I’ve really felt confident in. I spiked a quick rack-pagespeed [...]

  3. Zoltan Olah says:

    Wow, that’s really painful, you should *not* have had to come up with a solution to that!

Leave a Reply

  • Search: