Lightning-Fast Sass Reloading in Rails

How to configure a Rails project with LiveReload and speed up your front-end workflow.

Imagine you are editing an SCSS file in your Rails application. You have your app open in a few web browsers for testing: Chrome on the main display, IE9 VM on the second monitor, and your iPhone and iPad on the desk. As soon as you hit “save” in your editor, your SCSS changes are compiled, and pushed out to all these browsers. Instantly. The browsers don’t even reload the page; the new styles simply pop in. All of this takes less than a second.

“But isn’t the asset pipeline too slow?”

A fast workflow like this might seem impossible in a typical Rails project. Until recently this was my experience working with Sass in Rails: edit SCSS file; save; switch to browser and hit refresh; wait for Rails to route, query the database, and render my ERB; now wait for what seems like 5 seconds for the asset pipeline to recompile everything; yawn; oops that’s not the style I expected; back to editing SCSS; repeat.

Sound familiar?

It’s true, unfortunately: out of the box and in development mode, Rails is way too slow for editing and previewing Sass. But you can absolutely turn this around. Here are my secrets:

  • Use guard-livereload with rack-livereload to automatically push new styles to the browser
  • Make sure your Guardfile enables style injection by using the right regular expressions
  • Embrace the Sprockets //= require directive (yes, even in SCSS)
  • Be conscious of your @import statements and use them sparingly

Style injection FTW

Before going any further, let’s review what I mean by style injection. As Chris Coyier explains in his Style Injection is for Winners post on CSS Tricks:

By “style injection”, I mean being able to see styling changes immediately after authoring them without having to manually refresh your browser window… it’s so awesome that if it’s not a part of your workflow you should consider updating yours to include it.

Style injection is not entirely new, but it is becoming easier now that user-friendly tools like CodeKit and LiveReload are gaining in popularity. How do we get the benefits of style injection within an existing Rails project?

Answer: guard-livereload (if correctly configured).

Basic Rails livereload setup

First, make sure your standalone stylesheets are in app/assets/stylesheets (i.e. using the asset pipeline) and are named with the .css.scss extension. For example:

app/assets/stylesheets/application.css.scss

Partials should use a leading underscore and the .scss extension, like this:

app/assets/stylesheets/_variables.scss

Then, add the guard, guard-livereload, and rack-livereload gems to the :development section of your Gemfile. Since I’m on a Mac, my Gemfile also includes rb-fsevent for best Guard performance. Don’t forget to run bundle install.

group :development do
  gem "guard", ">= 2.2.2", :require => false
  gem "guard-livereload",  :require => false
  gem "rack-livereload"
  gem "rb-fsevent",        :require => false
end

Note that all guard-related gems are marked :require => false. This is because they are only needed when running guard on the command line; they aren’t used by the Rails app itself.

Next, enable rack-livereload by adding it to your development middleware stack. Add this config to config/environments/development.rb:

Rails.application.configure do
  # Automatically inject JavaScript needed for LiveReload
  config.middleware.insert_after(ActionDispatch::Static, Rack::LiveReload)
end

Finally, set up the necessary guard-livereload configuration by running guard init livereload. This will create or append to the Guardfile in your project.

If necessary, modify the contents of the :livereload block in the Guardfile to match this example:

guard :livereload do
  watch(%r{app/views/.+\.(erb|haml|slim)$})
  watch(%r{app/helpers/.+\.rb})
  watch(%r{public/.+\.(css|js|html)})
  watch(%r{config/locales/.+\.yml})
  # Rails Assets Pipeline
  watch(%r{(app|vendor)(/assets/\w+/(.+\.(css|js|html|png|jpg))).*}) { |m| "/assets/#{m[3]}" }
  watch(%r{(app|vendor)(/assets/\w+/(.+)\.(scss))}) { |m| "/assets/#{m[3]}.css" }
end

Restart your Rails application (e.g. stop and start Pow or rails server) and then run the following command to start Guard’s livereload monitor:

guard -P livereload

The -P option is a recent addition to Guard that allows you to specify which Guard plugin to execute. If you have a large Guardfile with other time-consuming plugins (e.g. rspec), using -P livereload ensures that only livereload will be executed, making things much faster.

You should see Guard start up and the livereload monitor should begin listening for connections:

14:36:48 - INFO - LiveReload is waiting for a browser to connect.

Now access your app from a web browser. The livereload connection between your browser and guard-livereload will be established, and you’ll see this message from Guard on the command line:

14:36:50 - INFO - Browser connected.

Congratulations! Now you can edit CSS and Sass in your asset pipeline and your changes will be automatically compiled and refreshed in the browser as soon as you hit “Save”. Give it a shot.

Digging deeper

Now that you have livereload working in your Rails app, you may be wondering what is going on behind the scenes. Why do we need both guard-livereload and rack-livereload?

How Guard and livereload work together

Live reloading is accomplished by serveral different pieces working closely together:

  • The guard command line process
  • The filesystem where your Sass source files are located
  • Your Rails app
  • And of course, the browser viewing your app

Guard watches the filesystem for changes to your SCSS files. It also listens for web socket connections from browsers running livereload.js.

Rails serves out CSS via the asset pipeline by compiling your SCSS files when CSS is requested by the browser. The rack-livereload middleware automatically inserts livereload.js into the <head> of every page.

Upon first loading a page of your app, the browser executes livereload.js, which in turn establishes a web socket connection to guard. Now livereload.js waits for messages indicating CSS files have changed.

Livereload diagram
An overview of live reloading using guard-livereload and rack-livereload.
Numbers 1–5 trace the style injection from start to finish.

Style injection, step by step

Now that you can see the pieces that are involved, let’s trace what happens during style injection, from the time you hit “save” in your source code editor (these steps refer to the red numbers in the diagram above).

  1. You make an edit to a.css.scss and save the file.
  2. Guard notices the file changed, and further is aware that a.css.scss corresponds to what the browser knows as a.css. It sends a message via the web socket that a.css has changed.
  3. The livereload.js code in the browser receives the message that a.css is out of date. It makes an HTTP request to Rails in order to get the new version of the stylesheet.
  4. The Rails asset pipeline receives a request for a.css, which it knows corresponds to a.css.scss. The SCSS file has changed since the last compile, so Sprockets recompiles the SCSS and delivers the new CSS back to the browser.
  5. The livereload.js code in the browser replaces the original a.css stylesheet with the new version it received. The rendered page updates to reflect the new styles. Style injection is complete.

Making it lightning-fast

You may notice that even though livereload is working, it still takes a few seconds or more for your changes to appear in the browser after you edit a Sass file. In my experience this is due to a few problems:

  • Your stylesheets are arranged in such a way that even a small change in one file causes everything to be recompiled, which is slow
  • You are using one or more big Sass libraries, which slow down compilation
  • Style injection is not working, so your browser has to refresh the entire page every time you make a stylesheet change

Let’s address these one by one.

Use Sprockets to arrange stylesheets

The Rails asset pipeline encourages developers to break their Sass into several files, which are then merged into a single file in production. Multiple files means you can better organize and modularize your code, but the resulting compilation and merging steps can slow you down, especially if it means touching one Sass file causes everything to be recompiled.

My solution is to use Sprockets to merge separate Sass files, and only use @import when absolutely necessary. (This has other advantages besides just speed; see my SMACSS and Rails post.)

Here’s an example application.css.scss:

//= require normalize
//= require ./base
//= require ./layout
//= require_tree ./modules

Notice I’m using //=require instead of @import. This means that each file will be compiled independently, and the resulting CSS will be merged. In terms of livereload, that means if I change layout.css.scss, only that one file needs to be recompiled. If I had used partials (i.e. _layout.scss) and @import, my entire application.css.scss and all stylesheets it imports would need to be recompiled. This can make a difference for a large project.

However, what you gain in speed you lose a bit in convenience. If you want to share variables and mixins across multiple stylesheets, //=require will not work. The solution? Place the imports on the top of each stylesheet. For example, my base.css.scss and layout.css.scss might both need these imports:

@import "globals/variables";
@import "globals/functions";
@import "globals/mixins";

If you find yourself repeating the same imports over and over, consider consolidating them into a single import. For example, I often create a globals/_all.scss with all my imports, and then my base.css.scss can be simplified:

@import "globals/all";

Avoid importing large libraries

Many of us love using Compass or Bourbon because they make CSS3 easy. More to the point, these libraries bring a huge number of helpful variables, functions, and mixins. Your project probably has an import like this:

@import "bourbon";
// or @import "compass";

Unfortunately the blessing of these frameworks is also a curse: imports in Sass (and inside the Rails asset pipeline in particular) are slow. That @import "compass" might look like a single import, but at compile time it expands to dozens: compass imports compass/css3, which in turn has 19 imports, each of which import their own utilities.

You’d think that these frameworks could somehow be compiled and cached so that the import penalty only hits once. But for whatever reason, it doesn’t work that way in practice: every time you edit your own stylesheet, you pay the price of that maze of imports when your stylesheet is recompiled. Ouch.

Workaround? Import only what you need. Here’s an example:

@import "globals/all";

// from bourbon:
@import "addons/prefixer";
@import "css3/box-sizing";
@import "css3/inline-block";
@import "addons/font-family";

Again, we lose the convenience of the @import "bourbon" catch-all, but the resulting speed gains are quite significant. This is the single biggest speed gain I’ve encountered when optimizing my Sass projects.

Ensure style injection works

Making a Sass change should not cause a full page refresh. Instead, the new styles should be “injected” with no refresh. You can verify this in Safari, for example: with a full refresh, you will see the blue loading bar indicator. If style injection is working, there will be no blue bar (and of course, your changes will be reflected more quickly).

If injections are not working, first make sure you have the latest versions of guard-livereload and rack-livereload (use bundle update if necessary).

Next, check your Guardfile. These are the magic lines that makes injection work:

watch(%r{(app|vendor)(/assets/\w+/(.+\.(css|js|html|png|jpg))).*}) { |m| "/assets/#{m[3]}" }
watch(%r{(app|vendor)(/assets/\w+/(.+)\.(scss))}) { |m| "/assets/#{m[3]}.css" }

Here’s how to interpret it:

  • The regular expressions tell Guard to monitor all your CSS files (among others) in the standard asset pipeline locations: app/assets and vendor/assets. Make sure your Sass files have .css in the filename (e.g. layout.css.scss) or end with .scss (e.g. _variables.scss); otherwise the default regular expressions will not match, and Guard will not monitor them.
  • The block translates the full file path (e.g. app/assets/stylesheets/base.css.scss) into the path that the browser actually uses to request that asset (in this case, /assets/base.css). If this translation is incorrect, injection may not work.

If your Guardfile looks different, update it to use the watch declarations as shown above.

Conclusion

Setting up a livereload workflow in Rails takes a few different gems, the proper configuration, and some changes to how you may have been writing or organizing your Sass files.

Is it worth it? In my experience, absolutely. Once you’ve gotten used to fast livereload with style injection, there is no going back.

Share this? Copy link

Feedback? Email me!

Hi! 👋 I’m Matt Brictson, a software engineer in San Francisco. This site is my excuse to practice UI design, fuss over CSS, and share my interest in open source. I blog about Rails, design patterns, and other development topics.

Recent articles

RSS
View all posts →

Open source projects

mattbrictson/rails-template

App template for Rails 7 projects; best practices for TDD, security, deployment, and developer productivity. Now with optional Vite integration! ⚡️

1,055
Updated 1 month ago

mattbrictson/tomo

A friendly CLI for deploying Rails apps ✨

360
Updated 20 days ago

More on GitHub →