A lot has been written about switching from vanilla CSS to a CSS preprocessor, and for good reason—preprocessors add power and control we can’t get in the browser alone. From articles that extol the virtues of a preprocessor to more technical reads like Etsy’s detailed “Transitioning to SCSS at Scale,” I feel like I’ve devoured them all.
At Sprout Social, we did something that hasn’t been written about nearly as much—switching from one preprocessor to another. Early on, Sprout adopted Less; we made the decision late last year to switch to SCSS, the CSS-like syntax of Sass. We took our time to ensure that the transition was smooth, and the experience highlighted some deep differences between Less and Sass.
Before we get to what we learned, your first question—a legitimate one—should be, “Why bother?” We already benefitted from variables and mixins, @imports and color functions. Certainly, Sass has a number of features Less lacks, such as maps and functions, but we made it this far without them.
Two major reasons for switching stand out:
- Community: Search github for lib extension:scss, then search for lib extension:less. As of this writing, the results are clear: 120,234 SCSS files, 29,449 Less files. Switching offers access to a wider array of good ideas and a larger open-source pool to swim. Even the popular Bootstrap library, one of the reasons Less has remained viable, has announced it is switching to SCSS.
- Speed: Libsass rocks. The build time for our styles improved by over 500%. While Libsass has not yet caught up with the latest version of the Sass spec, we do not feel we are missing anything.
Our compiled CSS has nearly 19,000 selectors. After the switch, that compiled CSS needed to be nearly identical; we had to ensure this transition was invisible to our customers. And what about features currently in progress? Our recent compose update changed 3,837 lines of styles—how could that team safely switch midstream?
We considered three options:
- Compile everything to CSS first. It’s the only way to ensure with 100% accuracy that our users were getting the same styles and to actually pull off the switch in one day. The idea of a clean break is always enticing, but new code is not always better code. Even with tools such as sass-convert and css2compass, the time we would spend rebuilding would greatly outweigh any other options.
- Only write new styles in SCSS. We considered drawing a line in the sand—Less is old and busted; Sass is the new hotness. Ultimately, we rejected this notion. So much would be gained from switching immediately, and no one wanted to maintain parity between two sets of mixins and variables.
- Convert all of our Less files to SCSS and fix what breaks. Faced with either throwing out history or adding another build task to maintain, we set about converting everything.
SCSS is not that different from Less, is it? “Converting from Less to Sass” shares a series of regex searches to change the most obvious syntax differences, such as .awesome-mixin() vs @mixin awesome-mixin(). Those regex searches are rolled up in less2sass, which we ran through all of our files.
If it were that easy, though, there really wouldn’t be much to blog about. A few lingering pull requests to the less2sass script emphasize some of its oversights, such as string interpolation differences. More challenging were the build errors we encountered after the conversion, which highlighted differences greater than a simple regex could resolve. To be frank, we also found some bad CSS.
We took those build errors and made a list of what we needed to fix, and we knew we could fix most of it before converting the styles. We decided to clean up our Less files before converting.
We started with the difference between how Less and Sass handle conditionals. Here’s a simple gradient mixin we had:
Sass offers a simple @if…@else structure, whereas our mixin employed what Less calls a mixin guard. In the case of our gradient mixin, we were using it to change from the vendor-prefixed draft syntax to the W3C syntax. We knew we’d have to rewrite this mixin.
Then, we stopped and took a long look at all of our mixins. Most of them added vendor prefixes and resolved browser differences such as the gradient mixin above. Enter Autoprefixer, a tool that parses CSS and applies vendor prefixes based on a list of supported browsers. By adding Autoprefixer to our build, we eliminated nine mixins. As a bonus, Autoprefixer removes unneeded vendor prefixes, which helped us identify some dusty corners in our CSS and produce smaller compiled files.
A good lesson from our experience here: Don’t waste time converting or refactoring what you can delete.
Another mixin difference worth noting: Less recommends separating parameters with semicolons. Only a few had been written this way, but they all had to be changed, in the mixin definitions and where they were applied. Fortunately, Less supports both semicolons and commas, so we could make this change before the conversion step.
After addressing mixins, we turned our attention to another source of build errors: ampersands. It’s one of the most powerful operators in both Sass and Less, and they work very similarly. Except when they don’t. And then they work very differently.
For example, with 19,000 selectors, you can imagine that we run into specificity problems, often quickly solved as such:
Less produces h1.modal-header as one would suspect, but Sass chokes. We tried fixing it with:
Works great with Ruby Sass, but as of this writing, Libsass does not yet support this use. We didn’t even consider, in this case, switching to Ruby Sass. We instead wrote out h1.modal-header outside of the scope of .modal. We know that this is an indication of a problem, so by pulling the selector out of the scope and calling it out with a comment, we can identify those issues in our code more readily.
It got worse when an ampersand was used in this way in a mixin. Here’s an excerpt of a Less mixin we had for buttons:
Again, the @at-root directive couldn’t help us in Libsass. In this case, we had to look at the root cause of the specificity override and resolve it. (The good news is that we fixed it by deleting three overly-specific styles elsewhere.)
Another difference between Less and Sass ampersands was actually helpful:
Our expectation was .checkbox-wrap > .checkbox-widget, .radio-wrap > .radio-widget. However, Less processes the ampersand with more recursion and compiled thusly:
At no point did we—or would we—use a checkbox widget for a radio button. Fortunately, Sass actually resolved a problem we didn’t know about because we weren’t looking at our compiled CSS.
Lesson learned: Look at your compiled CSS often—otherwise, you don’t know what your users are downloading.
Comparing the Results
The updates to fix and remove mixins, resolve ampersand discrepancies and address some other bits that weren’t going to convert cleanly occurred across seven commits over the course of a month. It felt good to clean house and identify future refactoring opportunities.
Yet it doesn’t matter what our source code looks like; it’s what gets delivered to our users that counts. We considered generating ASTs to compare our compiled CSS. After some research and experimentation, it became clear that all we needed was a way to know if very little had changed in the compiled CSS. Therefore, good old-fashioned diffs would suffice—the smaller the diff, the better. Each pull request came with a diff of the before-and-after results of the Less compilation. The Xcode developer tool FileMerge was very handy to compare results side by side. If we saw anything we didn’t expect, we went back to investigate.
We stuck with FileMerge and diffs once we went on our regex find-and-replace stampede and actually converted the files to SCSS. However, the results compiled by two different preprocessors made our diffs useless because of differences in tabbing and bracket placement. We added an extra step to normalize the format of the before-and-after CSS with a simple node script. It minifies the CSS, then beautifies it. Couldn’t be simpler.
Normalizing the formatting helped greatly, but combing through the diff still took about two solid days of reviewing. A rewarding process but arduous. We doubt that a custom AST solution would have helped speed up the review. All differences had to be addressed.
But the differences were minor. Selectors in a slightly different order, decimal rounding and even slight differences in the results of color functions. Each difference was checked carefully before pushing our Sassed-up CSS into production.
Once merged, in-progress work hardly stalled. Less files still in development were easy to convert, thanks to all the prep work done ahead of time. Everyone was up and running in about two days. Even the redesigned Compose team was able to regex its way to SCSS in a matter of hours. Planning ahead and cleaning up existing styles before pulling the switch made all the difference.
Now we’re moving on with identifying patterns, breaking up large CSS files into modules, auditing CSS in production for unused selectors and spending more time on tools to compare ASTs or some other parsed representation of our CSS. Did I mention we have nearly 19,000 CSS selectors? We’re on it—but that’s another article entirely.
Lost in translation: Upleveling Sprout Social’s localization systemPublished on June 1, 2023 Reading time 7 minutes
Lessons learned as an entry-level software engineerPublished on August 30, 2022 Reading time 5 minutes