Transitioning to SCSS at Scale

Posted by on February 2, 2015

Naively, CSS appears easy to comprehend — it doesn’t have many programming constructs, and it’s a declarative syntax that describes the appearance of the DOM rather than an executable language. Ironically it’s this lack of functionality that can make CSS difficult to reason about. The inability to add scripting around where and when selectors are executed can make wide-reaching changes to CSS risky.

CSS preprocessors introduce advanced features to CSS that the current iteration of the CSS specification does not.  This functionality commonly includes variables, functions, mixins and execution scope, meaning that developers can embed logic that determines how CSS is written and executed.  If correctly applied preprocessors can go a long way towards making CSS more modular and DRY, which in turn result in long-term maintainability wins for a codebase.

One of the goals of the Front-end Infrastructure Team for 2014 was to fully transition the CSS codebase at Etsy to SCSS [1]. SCSS is a mature, versatile CSS preprocessor, and Etsy’s designers and developers decided to integrate it into our tech stack.  However, we knew that this effort would be non-trivial with a codebase of our size.  As of October 2014, we had 400,000+ lines of CSS split over 2000+ files.

In tandem with a team of designers, the Front-end Infrastructure Team began developing the processes to deploy SCSS support to all development environments and our build pipeline. In this post I’ll cover the logic behind our decisions, the potential pitfalls of a one-time CSS-to-SCSS conversion, and how we set up tooling to optimize for maintainability moving forward.

Why SCSS?

The biggest validation of the potential for SCSS at Etsy was the small team of designers beta-testing it for more than six months before our work began.  Since designers at Etsy actively push code, a single product team led the initial charge to integrate SCSS into their workflow.  They met regularly to discuss what was and was not working for them and began codifying their work into a set of best practices for their own project.

It was through the initial work of this product team that the rest of the company began to see the value and viability of introducing a CSS preprocessor.  The input of these designers proved invaluable when the Front-end Infrastructure Team began meeting to hatch a plan for the deployment of SCSS company-wide, and their list of best practices evolved into an SCSS style guide for future front-end development.

After evaluating the landscape of CSS preprocessors we decided to move forward with SCSS. SCSS is an extremely popular project with an active developer community, it is feature rich with excellent documentation, and because the SCSS (Sassy CSS) syntax is a superset of CSS3, developers wouldn’t have to learn a new syntax to start using SCSS immediately. With regards to performance, the Sass team prioritizes the development and feature parity of libsass, a C/C++ port of the Sass engine [2]. We assumed that using libsass via the NodeJS bindings provided by node-sass would enable us to integrate an SCSS compilation step into our builds without sacrificing speed or build times.

We were also excited about software released by the larger SCSS community, particularly tools like scss-lint. In order to compile SCSS we knew that a conversion of CSS files to SCSS meant remedying any syntactical bugs within our CSS code base.  Since our existing CSS did not have consistently-applied coding conventions, we took the conversion as an opportunity to create a consistent, enforceable style across our existing CSS.  Coupling this remediation with a well-defined style guide and a robust lint, we could implement tooling to keep our SCSS clean, performant and maintainable moving forward.

An Old and New CSS Pipeline

Our asset build pipeline is called “builda” (short for “build assets”). It was previously a set of PHP scripts that handled all JS and CSS concatenation/minification/versioning. When using libraries written in other languages (e.g. minification utilities), builda would shell out to those services from PHP. On developer virtual machines (VMs) builda would build CSS dynamically per request, while it would write concatenated, minified and versioned CSS files to disk in production builds.

We replaced the CSS component of builda with an SCSS pipeline written in NodeJS. We chose Node for three reasons. First, we had already re-written the JavaScript build component in Node a year ago, so the tooling and strategies for deploying another Node service internally were familiar to us. Second, we’ve found that writing front-end tools in JavaScript opens the door for collaboration and pull requests from developers throughout the organization. Finally, a survey of the front-end software ecosystem reveals a strong preference towards JavaScript, so writing our build tools in JS would allow us to keep a consistent codebase when integrating third-party code.

One of our biggest worries in the planning stages was speed. SCSS would add another compilation step to an already extensive build process, and we weren’t willing to lengthen development workflows or build times. Fortunately, we found that by using libsass we could achieve a minimum of 10x faster compilation speeds over the canonical Ruby gem.

We were dedicated to ensuring that the SCSS builda service was a seamless upgrade from the old one. We envisioned writing SCSS in your favorite text editor, refreshing the browser, and having the CSS render automatically from a service already running on your VM — just like the previous CSS workflow. In production, the build pipeline would still output properly compiled, minified and versioned CSS to our web servers.

Despite a complete rewrite of the CSS service, with a robust conversion process and frequent sanity checking, we were able to replace CSS with SCSS and avoid any disruptions. Workflows were identical to before the rewrite and developers began writing SCSS from day one.

Converting Legacy Code

In theory, converting CSS to SCSS is as simple as changing the file extension from .css to .scss.  In practice it’s much more complicated.

Here’s what’s hard about CSS: It fails quietly.  If selectors are malformed or parameters are written incorrectly (i.e. #0000000 instead of #000000), the browser simply ignores the rule. These errors were a blocker on our conversion because when SCSS is compiled, syntax errors will prevent the file from compiling entirely.

But errors were only one part of the problem. What about intentionally malformed selectors in the form of IE-hacks? Or, what about making changes to legacy CSS in order to conform to new lint rules that we’d impose on our SCSS? For example, we wanted to replace every instance of a CSS color-keyword with its hex value.

Our conversion was going to touch a lot of code in a lot of places. Would we break our site by fixing our CSS?  How could we be confident that our changes wouldn’t cause visual regressions?

Conventionally there are some patterns to solve this problem. A smaller site might remedy the syntax bugs, iterate every page with a headless browser and create visual diffs for changes. Alternatively, given a certain size it might even be possible to manually regression test each page to make sure the fixes render smoothly.

Unfortunately our scale and continuous experimentation infrastructure makes both options impossible, as there are simply far too many different combinations of pages/experiments to test against, all subject to change at a moments notice.  A back of the envelope calculation puts the number of possible variants of Etsy.com at any time at ~1.2M.

We needed to clean any incorrect CSS and enforce new lint rules before we performed the SCSS rename, and we needed to confirm that those fixes wouldn’t visually break our site without the option to look at every page. We broke the solution into two distinct steps: the “SCSS Clean” and the “SCSS Diff.”

SCSS Clean

We evaluated various ways to perform the CSS fixes, initially involving an extensive list of regular expressions to transform incorrect patterns we identified in the code. But that method quickly became untenable as our list of regular expressions was difficult to reason about.

Eventually we settled on our final method: using parsers to convert any existing source CSS/SCSS code into Abstract Syntax Trees (AST), which we could then manipulate to transform specific types of nodes.  For the unfamiliar, an AST is a representation of the structure of parsed source code.  We used the Reworkcss CSS parser to generate CSS ASTs and gonzales-pe to generate SCSS ASTs, and wrote a custom adapter between the two formats to streamline our style and syntax changes.  For an example into what a generated AST might look like, here’s a great example from the Reworkcss CSS parser.

By parsing our existing CSS/SCSS into ASTs, we could correct errors at a much more granular level by targeting selectors or errors of specific types. Going back to the color-keyword example, this gave us a cleaner way to replace properties that specified color values as color-keywords (“black”) with their equivalent hexadecimal representation (#000000).  By using an AST we could perform the replacement without running the risk of replacing color words in unintended locations (e.g. selectors: “.black-header”) or navigating a jungle of regular expressions.

In summary, our cleaning process was:

  1. Generate an AST for the existing CSS/SCSS file.
  2. Run a script we created to operate over the AST to identify and fix errors/discrepancies on a per-property level.
  3. Save the output as .scss.
  4. Run the .scss file through the libsass compiler until the successful compilation of all files.
  5. Iterate on steps #2-4, including manual remediation efforts on specific files as necessary.

SCSS Diff

Cleaning our CSS was only half the battle. We also needed a way to confirm that our cleaned CSS wouldn’t break our site in unexpected ways, and to make that determination automatically across thousands of files.

Again we turned to ASTs.  ASTs strip away superficial differences in source code to core language constructs.  Thus we could conclude that if two ASTs were deeply equivalent, regardless of superficial differences in their source, they would result in the same rendered CSS.

We used our Continuous Integration (Jenkins) server to execute the following process and alert us after each push to production:

  1. Run the old builda process with the original, untouched CSS, resulting in the minified, concatenated and versioned CSS that gets deployed to production servers and the live site.  Build an AST from this output.
  2. Concurrently to step 1, run the SCSS conversion/clean, generating SCSS files from CSS.  Run these SCSS files through SCSS builda, resulting in minified, concatenated, and versioned CSS from which we could generate an AST.
  3. Diff the ASTs from steps 1 and 2.
  4. Display and examine the diff.  Iterate on steps 1-3, modifying the cleaning script, the SCSS builda code or manually addressing issues in CSS source until the ASTs are equivalent.

SCSSatScale_1

With equivalent ASTs we gained confidence that despite touching the thousands of CSS files across Etsy.com, the site would look exactly the same before and after our SCSS conversion.  Integrating the process into CI gave us a quick and safe way to surface the limits of our cleaning/SCSS implementations by using live code but not impacting production.

Going Live

With sufficient confidence via AST diffing, our next step was to determine how to deploy to production safely. Here was our deployment strategy:

SCSSatScale_2

Using the original CSS as source, we added the SCSS builda process to our deployment pipeline. On a production push it would take the original CSS, clean it, create SCSS files and then compile them to CSS files in a separate directory on our production servers. We continued to serve all traffic the CSS output of our existing build process and kept the new build flagged off for production users.  This allowed us to safely run a dress rehearsal of the new conversion and build systems during deployments and monitor the system for failures.

SCSSatScale_3

Once the SCSS builda process ran for several days (with 25-50 pushes per day) without incident, we used our feature flagging infrastructure to ramp up 50% of our production users to use the new SCSS builda output. We monitored graphs for issues.

SCSSatScale_4 After several days at 50%, we ramped up SCSS builda output to 100% of Etsy.com users and continued to monitor graphs.

SCSSatScale_5

The final step was to take a few hours to hold production pushes and convert our CSS source to the converted SCSS.  Since our SCSS builda process generated its own cleaned SCSS, transitioning our source was as simple as replacing the contents of our css/ directory with those generated SCSS files.

One 1.2M-line deployment later, Etsy.com was running entirely on SCSS source code.

Optimizing for Developer Productivity and Code Maintainability

We knew that integrating a new technology into the stack such as SCSS would require up-front work on our end with regards to communication, teaching and developer tools. Beyond just the work related to the build pipeline it was important to make sure developers felt confident writing SCSS from day one.

Communication and Teaching

The style guide and product work by the initial SCSS design team was key in showing the value of adopting SCSS to others throughout the organization. The speed at which new, consistent and beautiful pages could be created with the new style guide was impressive.  We worked with the designers closely on email communication and lunch-and-learn sessions before the official SCSS launch day and crafted documentation within our internal wiki.

Developer Tools and Maintainability

Beyond syntax differences, there are a couple of core pitfalls/pain points for developers when using SCSS:

  1. SCSS is compiled, so syntax errors explode compilation and no CSS hits the page.
  2. You can accidentally bloat your CSS by performing seemingly harmless operations (here’s looking at you, @extend).
  3. Nested @import’s within SCSS files can complicate tracing the source files for specific selectors.

We found the best way to remedy both was to integrate feedback into development environments.

For broken SCSS, a missing/non-compiling CSS file becomes an error message at the top of the document:

SCSSatScale_6

For maintainability, the integration of a live, in-browser SCSS lint was invaluable:

SCSSatScale_7

The lint rules defined by our designers help keep our SCSS error-free and consistent and are used within both our pre-deployment test suites and in-browser lint.  Luckily the fantastic open source project scss-lint has a variety of configurable lint rules right out of the box.

Lastly, due to nested SCSS file structures, source maps for inspecting file dependencies in browser developer tools were a must. These were straightforward to implement since libsass provides source map support.

With the SCSS build processes, live lint, source maps, test suite upgrades and education around the new style guide, our final internal conversion step was pushing environment updates to all developer VMs. Similarly to the SCSS production pipeline the developer environments involved rigorous testing and iteration, and gathering feedback from an opt-in developer test group was key before rolling out the tooling to the entire company.

Conclusions and Considerations

The key to making any sweeping change within a complex system is building confidence, and transitioning from CSS to SCSS was no different.  We had to be confident that our cleaning process wouldn’t produce SCSS that broke our site, and we had to be confident that we built the right tools to keep our SCSS clean and maintainable moving forward. With proper education, tooling and sanity checks throughout the process, we were able to move Etsy to SCSS with minimal disruption to developer workflows or production users.

  1. We use SCSS to refer the CSS-syntax version of Sass. For all intents and purposes, SCSS and Sass are interchangeable throughout this post.
  2. In order to maintain our old system’s build behavior and prevent redundant CSS imports, we forked libsass to support compass-style import-once behavior.
  3. Graphics from Daniel Espeset – Making Maps: The Role of Frontend Infrastructure at Etsy – Fronteers 2014 Presentation (http://talks.desp.in/fronteers2014/)

You can follow Dan on Twitter at @dxna.

Posted by on February 2, 2015
Category: engineering, infrastructure

18 Comments

Great article Dan. I was most impressed with these AST diff. My gut was telling me to diff screenshots taken with automated testing tools.

Thanks for the article, very interesting! Have you got any information about how the move to SCSS affected the file size of the css you’re shipping?

    At the time of the transition our process involving AST diffing resulted in effectively the same output CSS as our previous process, so there was no bloat in file sizes when we made the switch. For maintainability moving forward integrating tools like scss-lint and disallowing `@extend` are preventative against future bloat, and we work closely with the performance team to track the impacts of metrics like CSS payload size on load times across the site. So for all intents and purposes the move to SCSS didn’t have a significant impact on the file sizes of shipped CSS (and hopefully shouldn’t moving forward) but it’s definitely something to remain cognizant of when switching to a CSS preprocessor.

      I, too, love your AST comparison. At Dealer.com when we transitioned from Ruby Sass to Sass, we’d considered using an AST to compare the differences, but at the beginning of the process (that is, before our implementation of @extend in libsass) the noise from an AST diff would have been way too high. As soon as I read this post, however, I realized we should have switched to an AST comparison once we started getting closer. Thanks for documenting how to do it right!

      Regarding @extend: your decision to disallow it is wise. We needed @extend support because we had an good-sized codebase (~1100 scss source files) already using @extend, and refactoring that usage introduced a high level of risk. That doesn’t change the fact that @extend is a powerful and dangerous tool, that does the wrong thing (see https://gist.github.com/nex3/e9640a78e417c046afee) without much encouragement. I certainly recommend not using @extend. Are you using scsslint to disallow it in your codebase?

      Is there a reason you chose scsslint (Ruby) over something like csscomb (Node), when the rest of your build seems to be Node-centered?

      Thanks again for a great post.

      scss-lint provides a “PlaceholderInExtend” lint, which is explained in depth in this article (http://8gramgorilla.com/mastering-sass-extends-and-placeholders/) and perhaps the safest way to use @extend. However we decided that for our size and long-term maintainability to disallow any version of @extend entirely. Instead we integrated a simple shell script into our lint that looks for the presence of @mixin, @extend, @function, and variable declarations in places where they shouldn’t be and returns an appropriate error.

      We weren’t familiar with csscomb, but looking at it now it seems as if it performs similar operations to our clean scripts, which we only executed once for the CSS->SCSS conversion. The maintainer is the same author as the gonzales-pe SCSS parser which powered our own tooling. Our use of scss-lint isn’t intended to be used as an automatic code-remediation tool (like csscomb or our clean) but more of as a feedback loop to developers that their SCSS deviates from our defined best practices. In that manner the lint doubles as an education and maintainability tool.

      Above, it says “Ruby Sass to Sass” but that was meant to be “Ruby Sass to Libsass”.

      Thanks for your response, Dan. I hadn’t looked closely at gonzales-pe, and I’ll be sure to take a closer look now. When I was comparing ASTs, I was using the AST from PostCSS to compare output CSS, but I don’t know how well that would handle the SCSS syntax (if that was something I needed to parse, it seems more likely to be the output CSS I care about).

      I was thinking of csscomb’s linting option, https://github.com/csscomb/csscomb.js/blob/master/doc/usage-cli.md#lint – blocking builds for usage that doesn’t meet the lint criteria. If anyone happens to know of a csscomb lint setting to disable @extend (or extending non-placeholder selectors), I’d be happy to know about it, so I could take it off my to-do. 🙂

Whatever it was, the future is SCSS. So you have to get used to it.

[…] the Front-end Infrastructure Team for 2014 was to fully transition the CSS codebase at Etsy to SCSS [1]. SCSS is a mature, versatile CSS preprocessor, and Etsy’s designers and developers decided to […]

I’d like to know more about the structure you adopted for the CSS/SCSS.

I mean, if you had a legacy script which concatenated/minified files, you probably mimicked the same structure with SCSS imports – my question is: which kind of structure do you adopt for such a large codebase? Do you use SMACCS, Atomic Design or your own convention? Could you provide some examples?

Thank you very much for this article. It’s always interesting to learn how big organizations with a large codebase structure their code.

[…] Transitioning to SCSS at Scale […]

Thanks for an excellent article, Dan.

I’m facing a similar challenge in planning a move from static CSS to SASS for an existing enterprise-level web-based software product, though thankfully with slightly smaller/less complex overheads than Etsy.

Like Luca, I’d be interested in finding out a bit more about what structure/approach you decided to take with the SASS rewrite.

Thanks again for the write-up.. very useful in demonstrating to the powers that be that these kind of challenges aren’t insurmountable!

Any reason why you went with the SCSS syntax vs SASS? I find not typing all of the curlies and semicolons to be awesome. And it looks like your linter complains if things aren’t properly indented anyway, so why not just do away with the extra characters?

    The SCSS syntax is a superset of CSS3, meaning there’d be less of a barrier for devs throughout the organization to start writing SCSS immediately (as opposed to having to learn a new syntax). It appears to be a matter of style preference though, really. For what it’s worth, the Sass documentation refers to the indented style as the “older” style.

Great article, nice to see a real use case on a large application.

I was most interested into the maintainability part—in particular this one:

> You can accidentally bloat your CSS by performing seemingly harmless operations (here’s looking at you, @extend).

IMO, the greatest advantages in Sass also offer the largest risks (I’ve seen a single @extend generate extra 300KB worth of rules), and from what I’ve read you mitigated that through style guides. So I wondered: how many people touch the SCSS files on a daily basis? Would that process scale to the point where more than 50 people could work with Sass without introducing abusive nesting or @extends?

Great work Dan! It certainly takes prudence and good judgement to make such transition on the scale of Etsy.