📣Postmark has been acquired by ActiveCampaign
x

Refactoring >14,000 lines of CSS into Sass

Beanstalk is a mature product and during its’ 5 years of existence the design and UI have been changed a lot. Our CSS grows accordingly and lately it consisted of 5 files, 14,211 lines and 290 KB of code. We handcrafted our CSS from the start but more recently it had become quite hard to manage. With the growth of new tools like Sass and LESS I decided to rebuild our stylesheets into something cleaner and easier to maintain, and after working with the new system for a month I’m really happy with the results.

Our previous CSS organization

Keep in mind, Beanstalk was launched back in the days when Internet Explorer 7 was the latest version, conditional comments weren’t used much and mobile versions of applications barely existed. Since then our CSS had a few major refactorings and cleanups in addition to day-to-day maintenance, so it was in quite good shape, but the file structure still reflected these historical milestones:

  1. global.css — main stylesheet of the application
  2. print.css — print stylesheet
  3. mobile.css — mobile stylesheet with media queries, added last year
  4. ie.css — hacks for IE 7 (we dropped IE 6 support in Jan 2010)
  5. ie8.css — hacks and workarounds for IE 8

Internet Explorer stylesheets are loaded with conditional comments, so most of our users download only the first 3 files.

The problems with handcrafted CSS and multiple files

Our old CSS organization had two conflicting problems:

  1. It’s hard to collaborate on a 252 KB behemoth, which was our global.css. Merges are problematic and tools like blame can get slow.
  2. It’s easy to forget about mobile and print stylesheets when they’re in different files.

CSS itself had a few issues and spots for potential improvement, too:

  • Ghost selectors from non-existing UI elements.
  • Missed standard CSS3 properties and forgotten vendor prefixes.
  • Slow CSS selectors.
  • Redundant rules and validation errors.

Some of these problems were easy to fix with CSS preprocessors. I decided to go with Sass as it’s a more popular choice and ships with Rails 3.1. It was hard to choose between it and LESS as they’re both fantastic products, but I had to settle on something.

Getting Organized with Sass

I decided to go with a modular approach to CSS organization — every independent set of rules stored in it’s own file, together with related print and mobile styles. Based on Sass naming convention all partials start with underscore, and I grouped related partials together:

  • _integration.overview.scss
  • _integration.profile.scss
  • _integration.wizard.scss

I ended up with 56 partials for our styles and two additional files for variables and mixins. They all imported into global.scss, which is converted to CSS afterwards. As I prefer expanded output style and by default Sass uses nested, I prepared a simple Shell script to launch Sass, so my teammates can easily produce correctly formatted file:

sass --watch stylesheets/scss/:stylesheets --style expanded

sass.sh is stored in the repository next to stylesheets and can be run with a simple command (I run it with an Alfred script):

sh sass.sh

It’s important to keep variables and mixins in partials as they can be used in other files, like in IE stylesheet. Another good example of variables and mixins sharing is our administrative area — it’s stored in separated directory in design repository, but it’s design is based on application’s global.css with some additions. It’s styles and widgets are stored in admin.css, which overwrites some global rules and uses application’s variables and mixins:

  • administration
    • stylesheets
      • scss
        • admin.scss
        • ↪ _variables.scss
        • ↪ _mixins.scss
      • admin.css
      • ↪ global.css
  • application
    • stylesheets
      • scss
        • global.scss
        • _variables.scss
        • _mixins.scss
      • global.css

To import partials from another directory I use the load-path option:

sass --watch stylesheets/scss/:stylesheets ↩
     --style expanded ↩
     --load-path ../application/stylesheets/scss

(Later we’re going to just build admin.css from used partials without importing global.css at all.)

Steps to turn our CSS into a thing of beauty

After taking our CSS apart into multiple partials I started the cleanup and light refactoring process.

Improving color management with Variables and Functions

Sass variables are great for storing colors and font families. I actually really like to know where my colors are coming from, so I rebuilt some color values with Sass functions like darken and lighten. At some point I even converted a bunch of colors to RGBA just because they were so much easier to understand, but then decided that providing fallback RGB colors is too much hassle. This made me think — what I really wanted is not transparency of RGBA, but a way to generate one color from two with some transparency. For example, our tabs are white with 12% (normal state) or 31% (hover state) transparency over brown:

Blended colors in navigation

Turns out there is no built-in function for this calculation in Sass, so I had to write it myself — check it out on GitHub. It’s really easy to use — function blend(#534B38, rgba(#FFF, .12)) generates #67604F, but I recommend using it with variables for even better obviousness — blend($brown, rgba(white, .12)). It must be my most used color function in Sass.</code"> </p><h3>Removing Ghost Selectors with Regular Expressions</h3><p>Well, it happens — you redesign some element and forget to clean up old CSS afterwards, which makes the code bloated, increases file size and decreases performance. I spent a day checking if most of our CSS selectors existed in design mockups or development code. Consistent code formatting and simple regexps come in handy here. I used these ones the most: </p><ul> <li><code>[" ]classname — is this class name ever used?

  • h3.*classname — is this class name ever used with h3 element?
  • With these checks and refactoring I got rid of about 10% of our stylesheets, which is much more than I expected.

    Consistent Vendor Prefix Handling with Mixins

    Writing all vendor prefixes in a consistent way could be tiring and painful, so it was no wonder that I found lots of places where standard property or some existing vendor prefix was missed. I even found a dozen places where we used -khtml prefix, while we don’t really support Safari 2 or Konqueror. This problem is easy to solve with Sass mixins, so I created a bunch of them for the most popular rules. Now I can use one-line includes instead of 3-4 lines of vendor prefixes:

    @include border-radius(3px);

    generates these styles:

    -webkit-border-radius: 3px;
       -moz-border-radius: 3px;
            border-radius: 3px;

    Although Compass has a great collection of pre-build mixins I decided to start with our own toolkit and extend it as we go.

    Seek and Destroy any slow CSS selectors

    This topic is really out of scope of this article, so I’ll just recommend a couple of good starting points:

    Our CSS is not the fastest one out there yet, but this time I got rid of inefficient universal (*) and attribute ([attr='attr']) selectors. There is more we can do to speed up our rendering, but it’s a big task on it’s own and we’re working on it separately.

    Remove redundant rules and validator errors

    dl dt { 
        display: block;
        float: left; 
        vertical-align: bottom;
    }
    hr { 
        margin: 0em 0em 1em;
    }
    ul li { 
        display: inline; 
        width: 50px;
    }

    What’s wrong with all of these styles? They are redundant — floating element is always a block and vertical-align doesn’t apply to it, zero doesn’t need units and width or height don’t apply to inline element. These are just a few examples of possible code bloat and you can find more with CSS Lint and thoughtful code review. It’s always a good idea to check CSS with W3C validator, too — I found a few misspelled properties and values, even while I expected our code to be perfect. (Ha!)

    Goodbye IE7 support

    We dropped IE6 support two years ago when only 0.76% of visitors of our landing site were using it. Today Internet Explorer is used even less — only 0.1% of our visitors use version 7, 0.7% use version 8 and 1% use version 9. Hey, the total number of our IE visitors is getting really close to a number of our mobile users! We decided to drop IE7 support, but for some time we’ll still provide a file with hacks we built before.

    The Result: High Performance CSS

    What about front-end loading speed?

    • HTTP requests reduced from 3 to 1 (from 4 to 2 with IE 7-8).
    • File size reduced by 14% (or 38% when using the compressed output style).
    • Pages get loaded 0.5-1s faster.

    Not bad as for a byproduct of code refactoring, is it?

    But what’s even more important is that my performance is much better after all these changes. Our code is in much better shape. I have to write much less code thanks to mixins. We have a much cleaner merge process, as only changed Sass partials get merged. That said, there is one downside — now I terribly want to convert the rest of our sites and apps to the same system!