Speeding Up Grunt Builds Using Watchify

Grunt is a great tool for automating boring tasks. Browserify is a magical bundler that allows you to require your modules, Node.js style, in your frontend code.

One of the most useful plugins for grunt is grunt-contrib-watch. Simply put, it watches your file system and runs predefined commands whenever a change occurs (i.e. file was deleted, added or updated). This comes in especially handy when you want to run your unit tests every time anything changes.

Browserify works by parsing your JS files and scanning for require and exports statements. Then it determines the dependency order of the modules and concatenates them together to create a “superbundle” JS file. Hence every time your code changes, you need to tell browserify to rebuild your superbundle or your changes will not be picked up by the browser.

Watching Your Build: the Naive Approach

When connecting Grunt with Browserify, it might be tempting to do something like this:

watch: {
  sources: {
    files: [
      '<%= srcDir %>/**/*.coffee',
      '<%= srcDir %>/**/*.js'
    ],
    tasks: ['browserify', 'unittest']
  }
}

And it would work. All your code would be parsed and processed and turned into a superbundle when a file changes. But there’s a problem here. Can you spot it? Hint: all your code would be parsed and processed and turned into a superbundle.

Yep. Sloooooow.

On my MacBook Air with SSD and 8GB of RAM, this takes about 4 seconds (and that’s after I made all the big dependencies such as jQuery or Angular external). That’s a long time to wait for feedback from your tests, but not long enough to go grab a coffee. The annoying kind of long, in other words. We can do better.

Enter Watchify

Watchify is to Browserify what grunt-contrib-watch is to Grunt. It watches the filesystem for you and recompiles the bundle when a change is detected. There is an important twist, however, and that is caching. Watchify remembers the parsed JS files, making the rebuild much faster (about ten times faster in my case).

There’s one caveat you have to look out for though. When you’re watching your files in order to run tests (which you still need to do via grunt-contrib-watch because Browserify only takes care of the bundling), make sure you target the resulting (browserified) files and not the source files. Otherwise your changes might not get detected by Grunt watch (on some platforms Watchify seems to “eat” the file system events and they don’t get through to grunt-contrib-watch).

In other words, do something like this:

watch: {
  sources: {
     files: [
       '<%= buildDir %>/**/*.coffee',
       '<%= buildDir %>/**/*.js'
     ]
     tasks: ['test']
  }
}

where test is an alias for (for example):

test: [
  'mocha_phantomjs',
  'notify:build_complete'
]

You should see a huge improvement in your build times.

Happy grunting!

Tomas Brambora

Tomas Brambora