Transition to the Sass NPM Gem

tl;dr = Can we add “sass” to the default Webpacker configuration and drop the unmaintained rails-sass -> sassc -> libsass gem?

Hi Rails Team!

Over at @sass HQ, we’ve been busy over the last couple of years with a new implementation of Sass built in the Dart language, but with extensions and compilation targets in both native code and in JS.

We know it can get a bit confusing, but there are actually 3 possible “Sass” implementations that you might use in your Gemfile:

  • Ruby Sass (long deprecated, original implementation)
  • LibSass Powered Implementations– node-sass , sassc , and many other targets that use libsass in the background
  • Dart-Sass Powered Implementations– sass npm module, and brew install sass/sass/sass

Trust me, we know that this is a bit of a hot mess, but that’s what happens when you are a 14 year old language that’s evolved to become ubiquitous! (Also, fun fact, YESTERDAY was Sass’ 14th birthday!)

The Sass Core Team has deprecated both Ruby Sass, and all LibSass powered implementations. Why? Simply because libsass was unable to find a team of C++ developers to keep up development at pace with the version that Google sponsors (Dart-Sass). We had two amazing people trying their best part time, but just the weight of a C++ implementation is too much.

Pros of Changing

  • You can use Sass’ powerful module system
  • Far fewer bugs
  • Continuously added features
  • Quickly added support for new CSS features

Cons of Changing

  • LibSass/sassc gem is much faster than the Javascript-based sass npm module
  • There are slight incompatibilities, but only in extreme edgecases and in every instance, the reference implemenation handles it the preferred way- but it is different.

Also, Dart has recently added the ability to compile native versions of npm extensions, effectively becoming the same output as libsass. This will eventually make its way into the npm sass module, but at this moment, it’s slower.

As the person who started the libsass project years ago- in attempt to help Sass gain more market share, it pains me to ask to help us end the project faster, but the change is incredibly simple.

In all of my Rails projects, I’ve switched over and am extremely happy with the work that @nex3 and team have been doing on the Dart Sass implementation and the improvements across the board that they’ve made with the language’s stability. LibSass is no longer going to receive any updates or bug fixes.

I’m happy to write up the patch that would make the change in new projects.

Happy to answer any questions! Thanks!

4 Likes

Hey,

What would the story be to use Sass with the asset pipeline with such a change? The default configuration of Rails is to use Sprockets for SCSS, and only Webpacker for JavaScript. No appetite at the moment for changing that.

Also, gotta say that from our usage at Basecamp, I don’t think that I generally would be interested in trading speed for features. Sass does everything we want, but just barely fast enough :grinning_face_with_smiling_eyes:. How big is the performance regression?

Yeah, so this would basically be using webpack for the SCSS instead of the Rails Asset Pipeline. Which does have the nice advantage of enabling hot-reloading for your Sass files.

To answer the performance question is a little complicated. We definitely do have benchmarks here, that place the Dart-in-Node module at somewhere between 2-3x slower (whereas the native Dart version runs at-par on similar tests). I wouldn’t expect existing projects to make the switch yet, especially if the code base is very large.

But that’s not the whole story, because if you are writing net-new Sass and using the module system, we can write code that will eventually compile much more quickly. You see, Sass traditionally has had a “global” namespace, where every variable at the root of a file is interpreted as a global variable. And, every import is treated as an inline import.

Basically, the way your SCSS is written, it must be parsed and understood as a single large file, with significant ordering and no way to understand side-effects without a full recompilation. That’s hard on the CPU and even more difficult on RAM… think about how big of a string we have to build up!

By using the Sass Module System, we can write SCSS code that is explicit about the relationships between files. Likely today in your application.scss you might @import 'vars/colors'; and every subsequent file just assumes that every variable defined inside of vars/_colors.scss is available. A random chat.scss file will happily refer to $primary-color even though, it’s unclear where it came from. Worse, if halfway through your codebase, $primary-color is accidentally defined in a file, we have changed the value as a side-effect! Whoops.

With the module system, in the specific file that you want to use a color, you would do the following:

@use "vars/colors";

.chat-area {
  background: colors.$primary
}

Any file that wants access to the colors, needs to ask for it. @use functions as a specific call to import variables and mixins, and it functions as an @import-once. And therefore, the compiler can do a lot more optimization and determine compile order and more significantly– recompile order. Do I really need to recompile EVERYTHING?

We have just started to do these kinds of optimizations, and they will have an outsized impact on larger projects. But, unfortunately, they won’t be ‘backwards’ compatible, as every @import requires us to inline the contents.

Okay, this was a very long response, but I felt it was worth describing what we’re doing and why we are doing it. The biggest danger here for our language and Sass community, is that the new code that people are writing isn’t taking advantage of this cleaner, clearer way of constructing Sass documents.

It’s not that big of a deal that people need to go add ~3 lines to the webpacker config to switch over. But it does mean that people who type rails new will be starting with building projects with a syntax that isn’t going to be compatible with the majority of future Sass projects.

We will be directly addressing the performance problems with Dart’s Node implementation when we build in native extensions, but these structural changes to Sass are designed to set the stage for further enhancements.

But sounds like for now both the concept of doing CSS compilation in the Webpack build pipeline is off the table– I’m not entirely sure of the reasoning for the resistance to that, but hey, that’s your call not mine!

-hampton.

2 Likes

The features and the speed sounds great! But tying SCSS compilation to Webpack as a requirement is a non-starter for me. Sprockets and the asset pipeline is the default for CSS bundling in Rails, and no outlook to that changing in favor of Webpack.

Are there any options for us to use the Dart version from Sprockets?

2 Likes

It’s possible to build a gem that wraps the Dart Sass native version. There’s been a small team working on the Embedded Sass engine and also the Embedded Host Node.js wrapper.

At the moment, the team is only focused on bringing this to work and be compatible with node-sass (the libsass version for node and by far our most popular version). Specifically what makes it complicated and the need for that whole project is that there are many JS-extensions for Sass, because for node-sass we made an API where you can define custom functions in JS and then call them from your Sass code.

Then again, I’m not really sure how you all are using Sass. I see that sass-rails uses sassc, and I don’t think that does anything more than a straightforward compilation phase.

You could do a very, very, very simple wrapper around the Dart Sass and use dart2native to have static builds. It wouldn’t be extensible, but it would be do similar to what a command line compile could do.

-hampton.

Also, just to be explicit– while at my company we have added CSS processing to the webpacker/webpack pipeline, I have absolutely no love for webpack. In fact, I just hired the guy who is working on snowpacker and plan to continue development there.

But regardless of the ‘bundler’ or ‘unbundler’– it’s the same Node.js backend that is the one the Sass Team is supporting as our primary environment (for now).

This is exactly why I don’t have any interest in doubling down on SCSS processing through Webpack. I think we can see a future now where many Rails apps simply don’t need a JS bundler, because we have ESM in browsers, because of concepts like skycap.

So I’d like to see a simple wrapper around Dart Sass, if that’s where the work is happening. I don’t really personally care that much about extensibility for a default solution. It’s great to then ALSO have the node-sass option for people who’ve gone down the Webpack path. But it’s not a good default.

6 Likes