Subtle Live-Reloading With Grunt & Compass

 Filed on June 13, 2013

As a workflow nerd, I'm a huge fan of GUI tools like Codekit and Hammer, which automate basic development tasks like concatenating scripts, compiling CSS pre-processors, and auto-reloading the browser when changes are made to certain files: specifically, loading new CSS into the page after styles are edited in the code. Recently, I decided to take off the training wheels and learn the basics of Grunt.js to have more control over this process, and I hit a snag when I was setting up automatic browser reloading.

Usually, auto-reloading tools inject new styles into the page without requiring a full refresh – which is useful for making targeted, subtle tweaks without changing the state of the page or application. Unfortunately, the basic auto-reload setup most of the official Grunt examples show doesn't actually provide that feature, opting instead to automatically refresh the page after any change.

I'm not badmouthing the Grunt folks here – I cobbled this together from issue threads, comments and several different examples – and there are bound to be subtleties in the documentation that beginners like myself will miss. I'm going to lay out this Compass + LiveReload use case as straightforward as I can, all in one place. I will assume you know the basics of setting up Grunt itself, and focus on just the relevant parts of my Gruntfile. And if anything below is incorrect, embarrassing or could be done better, please find me on twitter and set me straight.


Here is the basic Gruntfile I started with, which uses LiveReload in conjunction with the grunt-contrib-watch and grunt-contrib-compass plugins:

module.exports = function(grunt) {

  // configure tasks
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    compass: {
      dist: {
        options: {
          sassDir: '_sass',
          cssDir: 'css',
          config: 'config.rb'
        }
      }
    },

    watch: {
      options: {
        livereload: true
      },
      css: {
        files: ['_sass/*.scss'],
        tasks: ['compass:dist']
      }
    }

  });

  // load plugins
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-compass');

  // register tasks
};

The particular section we're dealing with is this:

watch: {
  options: {
  livereload: true
  },
  css: {
  files: ['_sass/*.scss'],
  tasks: ['compass:dist']
  }
}

What I'm doing here is setting the watch task to check for changes in my _sass folder, where I have all my uncompiled stylesheets. When something is changed, Grunt runs Compass to compile the changes and then activates LiveReload. The behaviour that I expected (smoothly injecting new styles) doesn't happen – instead, the whole page refreshes.

Now, I don't know exactly why this happens, but thanks to some helpful threads on Github, I found a fix. As far as I can gather, the problem comes from the way I told the watch task to monitor various file changes. Regardless of the specific reason, the end result is the same: LiveReload has to load all the new CSS and then refresh the page, and can't determine what's changed and only inject the new styles.

The following is how I fixed it (credit to the linked Github issues above):

watch: {
  sass: {
    files: ['_sass/*.scss'],
    tasks: ['compass:dist']
  },
  css: {
    files: ['*.css']
  },
  livereload: {
    files: ['css/*.css'],
    options: { livereload: true }
  }
}

So, what's different here? I've split off my monitoring of Sass and CSS. When files inside my _sass directory are changed, Grunt runs Compass and compiles them, spitting out new files into the css directory. Separately from that, Grunt watches the css directory, and when files are updated it runs LiveReload. Something about the separation of tasks here achieves our goal: smoothly injected CSS styles without a full page reload.

Does this sound a lot like voodoo? It sure does to me. The lesson I learned is this: when you're responding to changes in a compiled file, don't watch for changes in that file's source. Watch the file itself. If anyone out there knows what's really going on here, please tweet me and let me know.