Learnings from using Sass in large scale projects
Different projects I have worked on used Sass as the pre-compiler of their choice. This post shares some learnings we had while using Sass. There are a couple of great articles on how to use Sass in large scale projects — this one is more of a retrospective. I hope it helps you solving problems if you ever run into them.
A recent project I worked on had a Sass compile time of around 12 seconds on my machine — or even more on other, less powerful, machines. When we saved a Sass file we needed to wait for Sass to compile with Compass and for some additional tasks as for example splitting CSS into two files to not hit IE’s 4096 selectors-per-file limit.
The team was not confident we could switch from Sass with Compass to use Sass without Compass. Or even better: Use libsass for our development workflow. This was since there were some custom functions based on Compass which we needed to ramp up our production system. Another reason for us using Compass was that we had a Bootstrap build which was based on it. The Sass files were deeply nested and thus Sass was working hard to figure out which file to load in which order.
The advantage of libsass is that it is a C/C++ implementation of Sass and thus a lot faster as Ruby, which is generally considered to not be a fast language.
Apart from libsass, the team would be able to remove Ruby as a general dependency of the project which reduces complexity. A libsassNode.js wrapper is available for everyone who is using Node.js as their build system (hi, Gulp and Grunt users).
When we collected ideas to speed up our build times we figured a couple of potential bottlenecks we might fix faster than switching to libsass. Especially splitting up our files and using files with a flat nesting level was something we thought might not only enable us to speed up development performance but also helps us achieving a better code structure. On the other hand we would need to invest the time to get all file imports straight and fix potential dependencies to mixins and functions.
As it turned out we were able to replace our Compass-influenced Ruby Sass workflow with libsass, a couple of mixins and some minor changes to the code base within a couple of hours (maybe three or so).
We did this change for our development workflow exclusively. Our production deployment remained on the old infrastructure for the time being.
We were able to reduce the Sass compile time to 2.5s without a pre-warmed cache and 1.4s with a cache, which is a saving of about 80% without cache and 88% with cache. This is amazing.
- If you can, avoid depending on Compass and even Ruby Sass. Sooner or later they will hold you back in developing fast.
- Avoid nesting @import rules deeply in order to have a streamlined structure. Apart from that you will benefit from a faster compile time.
Another project I worked on has a lot more code to handle than the aforementioned. What it does better though is the way it handles dependencies such as variables and mixins and the structure for imported files. Apart from that it is not using Compass. We are still running Ruby Sass because our continuous deployment server isn’t capable of compiling libsass.
But unfortunately we ran into other Sass-related problems:
Our performance budget for the project was set at around 100k CSS per page. Sounds quite nice. The product we were engineering was a tool box for 50 corporate websites with two different themes and custom fonts from a third-party provider. We wanted to deliver the websites really fast.
So we decided to include two CSS files per page. One of these files was the core holding the styles for styles that were needed on nearly all pages. The other one was a section specific file, for example for product listings or search styles which is loaded based on a defined section within one of the templates.
For easy extensibility within our components we used helper classes as for example a clearfix and some stuff for accessibility. They were defined as Sass’ extends, silent @extends (aka. placeholders) and mixins.
We thought some components might be useful to be implemented as placeholders since we only need them to be extended by certain variations of other components. For example buttons.
This was the theoretical part. As it turned out this wasn’t suitable for our large code base. The CSS selector chains and thus the file itself got quite big and we struggled with classes which should be defined within the specific page’s CSS but are part of the core and vice versa. Hard to find out which one was extended within the core and which wasn’t and why.
One solution was to include our helpers and some definitions of other extends within both core and the specific CSS. But this added even more bloat to the CSS we deliver and of course we really wanted to avoid this.
The next step was to implement a tool which first includes all mixins and extends we need and then strips out stuff we don’t use later. Phew… kind of hacky but it worked for the moment.
Why do we need to do all this?
- Prevent errors when building Sass because of @extend and @mixin
- Hassle-free development
- Deliver as little CSS as possible
In general: better performance of both development and network load.
We launched five sites. Besides our time to first byte (TTFB) we did a great job of delivering the front-end fast. TTFB wasn’t our problem but one of the back-end and DevOps team.
One of my colleagues, Mark, came up with the idea to test how much CSS we would deliver more if we save this one extra request for the site specific CSS and include everything within core: 10kb. BOOM! This was an easy decision for us. 10kb is pretty small.
In HTTP/1.1 land the browser needs to make one request to the server to get one file. The browser has to handle some overhead when downloading the file: Looking up the DNS, sending the request and waiting for the server to answer. If you use SSL for your site there are some hand-shakes involved, too. After these steps the file is being downloaded.
If you save one trip to the server with all the overhead and just send a file which is 10kb bigger than another one, which is maybe around 150kb, you will be pretty sure to save server resources and time until the user receives the file on the client side.
Usually you send two files instead of one because you want to parallelize both requests by sacrificing the second request. But since latency especially on mobile phones is a thing and we cannot send multiple files through one connection (this will be possible with HTTP/2) there is no advantage in sending 10kb less but loading another file which has even more than 10kb included.
Applying the changes Mark suggested made us aware of our problem: we had not worked as intended, we included different core components within site specific CSS files and vice versa.
Soon after our discovery that this worked for us we ran into a new issue: the 4096 selector size limit in IE8. This meant all of our aforementioned work to use just one file was invalid. We had to split our CSS once again into two files. A Grunt task called grunt-bless did this job for us.
Until now we have not found a decent solution other than reducing our selector count. A solution we did not implement is wrapping two stylesheets into Conditional Comments for IE8 and serve just one file for all modern browsers. We will invest more time in the future to test this.
There is always a journey when you work on large scale projects. A lot of things have been approved by the community and were tested before — but you will always encounter things that are unique to your project or don’t work for you as they do for others.
I believe sharing what you learned and problems you have helps others and prevents them from running into the same deadlocks you were.
Have you had projects where you ran into problems with building your Sass? Which were these? Share them!
An article I found helpful in regards to the whole topic “Sass at Scale” is Etsy’s article “Transitioning to SCSS at Scale”.