Transitioning to SCSS at Scale
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 . 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?
- An Old and New CSS Pipeline
- Converting Legacy Code
- SCSS Clean
- SCSS Diff
- Going Live
- Optimizing for Developer Productivity and Code Maintainability
- Conclusions and Considerations
- Footnotes and Links
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 . 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.
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.”
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:
- Generate an AST for the existing CSS/SCSS file.
- Run a script we created to operate over the AST to identify and fix errors/discrepancies on a per-property level.
- Save the output as .scss.
- Run the .scss file through the libsass compiler until the successful compilation of all files.
- Iterate on steps #2-4, including manual remediation efforts on specific files as necessary.
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:
- 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.
- 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.
- Diff the ASTs from steps 1 and 2.
- 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.
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.
With sufficient confidence via AST diffing, our next step was to determine how to deploy to production safely. Here was our deployment strategy:
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.
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.
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:
- SCSS is compiled, so syntax errors explode compilation and no CSS hits the page.
- You can accidentally bloat your CSS by performing seemingly harmless operations (here’s looking at you, @extend).
- 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:
For maintainability, the integration of a live, in-browser SCSS lint was invaluable:
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.
Footnotes and Links
- We use SCSS to refer the CSS-syntax version of Sass. For all intents and purposes, SCSS and Sass are interchangeable throughout this post.
- 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.
- 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.