Faster SASS builds with Webpack

If hot reload is part of your CSS development workflow, compilation performance is critical. That 5-second compile time in your large application is now a liability.

You're already enjoying a more efficient workflow if you're using hot reload for CSS. You no longer have to wait for full page reloads after every change.

Optimizing like this is addicting. The couple of seconds your SASS takes to compile is no longer acceptable. Every millisecond counts!

Enter Webpack.

Webpack is amazing. It allows you to organize CSS as you would JavaScript, but that power comes at a performance cost. Other build tools such as Gulp can be much faster. Fortunately there are ways to squeeze some speed out of it.

If you're not already using hot reload and style injection, you should!

Is style build time the problem?

The first step is to confirm what is slowing down compilation. CSS is a common culprit, but not the only one.

Webpack provides a fantastic profiler. To use it, first run webpack in profiler mode and output the statistics to a file: webpack --progress -c --profile --json > stats.json, then upload it.

You'll get to see a cool dependency tree, and various benchmarks on how long each modules took to process. If you see a lot of time spent in your CSS or SASS, keep on reading.

Webpack is very good at building JavaScript. It will rarely be the bottleneck, but you should still confirm it with the profiler before going down the rabbit hole.

Start with upgrading node-sass

If you're using node-sass, this is the most important step. Since version 3.X, node-sass has been one of the most robust CSS pre-processors available.

Unfortunately, libsass 3.2, used in node-sass 3.3 introduced a significant performance regression.

sassc is the command-line driver the libsass team uses to test libsass directly (since libsass is just a library). The above benchmarks were used to try and isolate if the performance regression was node-sass specific or if it was in libsass.

Not only was it fixed in 3.3 (part of node-sass 3.4), libsass is now faster than it has ever been, and is one of the fastest of the major pre-processors, neck and neck with PostCSS.

In our dashboard's codebase, the time spent in node-sass went from 2.7s~ with node-sass 3.3 to as little as 700ms in 3.4, a 74% boost!

The css-loader problem

Webpack's css-loader introduced modules in version 0.15, and with it introduced a massive performance regression.

In the bug report, a css-loader user reported that 0.18 took 60 seconds to compile their code, while 0.14.5 took only 16. Further investigation showed that the regression was introduced in 0.15. If you don't use CSS modules, 0.14.5 still works fine with the latest verson of Webpack as of this writing (0.12.2).

Webpack's css loader is responsible for handling sourcemaps as well as resolving urls and paths in @import statement.

Sourcemaps: choose your poison

Sourcemaps are a great tool to handle compiled languages, though they have a significant cost during compilation. Running webpack against our SASS (no javascript) with inline sourcemaps took 4.6 seconds. Turning them off reduced compilation time to 2.35 seconds: almost half the time!

This is unfortunate: Webpack's sourcemap support is amazing (when using inline sourcemaps, it just works, with no effort on the side of the developer. They just show up in the Chrome inspector).

We ran a quick survey in our frontend Slack channel asking if developers felt sourcemaps were worth a 100% compilation time increase.

In the end, we decided to side with performance.

You don't have to give up all your debugging options in the name of speed, however. node-sass provides another option: source comments. When source comments are enabled, node-sass will include a comment above each selector in the output referring to the original source. For example:

/* line 8, /Users/Fward/foo/bar/baz/shared.css */
li {  
  list-style: none;
}

This way, developers can look at the generated css, but still find where a style originated from. Source comments are also much faster than source maps, since they're simpler. We are able to compile our SASS in 2.5s with source comments: a great compromise between performance and ease of debugging.

Confirm if you need css-loader

You may be able to squeeze even more performance by getting rid of css-loader altogether. The css loader's primary purpose is to use webpack's path resolving and rewriting abilities for @import statements and urls. In production, it's very important (inlining images as base64 urls, cache busting by putting hashes in file paths and rewriting urls to point to your CDN). In development though, if your folder structure works for regular CSS urls, you don't need any of this. That means if you're also using source comments instead of sourcemaps and don't use CSS modules, you don't need css-loader at all and can use raw-loader instead.

The raw-loader is just a passthrough for text, with no processing. By switching to raw-loader, we gained another 5-10%.

Using Webpack's watch option and development server.

If you're using hot reload for your css, you're already using a file watcher of some kind to pick up the change, but you should make sure to use webpack's own --watch option, or its development server.

Webpack will be able to cache some information and speed up your compilation even more. By using --watch, our compile time hovers between 1.7 and 2 seconds.

Breaking down your SASS and the @import problem

Let webpack handle combining your stylesheets.

A great feature of Webpack is JavaScript incremental compilation. Even if you have a massive codebase that takes several minutes to compile, you'll only have to wait a few hundred milliseconds when you modify a file. Webpack keeps track of the state of your code and only recompiles what changed.

Unfortunately with CSS things aren't as sophisticated. Every time you make changes to a file, Webpack will recompile all files that depend on it. If you're relying on a single entry file with a ton of @imports Webpack will have to recompile everything every time anything changes.

Instead of having a monolithic stylesheet, like

@import 'foo';
@import 'bar';
@import 'baz';
...

you can leverage Webpack's ability to just require your css where it's needed. Simply use: require('../stylesheets/foo.scss'); from JavaScript in the component that needs it. Using the ExtractTextPlugin, Webpack will combine them into 1 output file (or more, if you have multiple chunks). Webpack will no longer have to recompile everything for every change, drastically speeding up compilation after startup.

There are a few caveats with this solution:

Separating CSS from Javascript

Webpack has a little known feature allowing the use of an array of configuration in the webpack.config.js file. When using an array of configuration, Webpack will run multiple compilers, while still sharing internal resources such as file watchers for efficiency.

If your codebase wasn't built with Webpack from the beginning, you may still rely on the ExtractTextPlugin and a top level entry point for your sass code. Webpack will put the styles in the same bundle as the javascript, then parse the modules to extract it in an external file. The more javascript in the bundle, the longer this step takes.

We can speed up this operation by running two Webpack configurations: one for JavaScript, and one for CSS.

module.exports = [{  
  name: 'js',
  entry: {
    main: [
      './src/index'
    ]
  },
  ...
}, {
  name: 'css',
  entry: {
    styles: [
     './src/stylesheets/main.scss'
    ]
  },
  ...
}]

This isn't the "webpack way" of handling CSS. You should require your stylesheets in javascript modules that need them. However, if you have a legacy code base, this is a great way to save an additional 100-200ms.

Switch to Linux

Ha! No, seriously. Kind of. Building our SASS in a VM running Ubuntu on top of MacOSX turned out to be 2-3 times faster than building on the host.

Conclusion

While css hot reload is a powerful way to increase velocity of designers and developers alike, it introduces new challenges when it comes to css processors. Every second counts when you're trying to debug a styling issue with trials and errors.

Webpack, while powerful, has a large overhead over simpler tools such as gulp (in our testing, Webpack can add as much as 1.5 seconds in a large codebase). By ensuring your code is lean, your dependencies are up to date, and you don't use loader options you don't need, it is possible to keep things snappy without losing flexibility.

The "sunset tower crane" image above is Creative Commons CC0 licensed https://creativecommons.org/publicdomain/zero/1.0/deed.en