Go to main contentGo to footer
javascript
|
09 January 15

BazarJS: The Diaspora of Node.js Task Runners

The first installment of our BazarJS series exploring the world of SPAs (single-page applications)... today we talk about build tools and task runners!

Stefano VernaHead of DatoCMS

As mentioned in the previous post, the birth of Node.js, a JavaScript runtime environment able to live independently of the browser, was without a doubt a major turning point for the JavaScript world. Node.js was certainly not the first attempt in this direction (Rhino dates all the way back to 1997), but it was definitely the first to take off and make a difference.

Rake, Make, Gradle, Ant... every programming language comes with its own build tool of choice, and Node.js was the environment that unlocked the possibility of building similar tools in the JavaScript world. It was an important milestone that finally made it possible to create processing tasks for our front-end JavaScript files, reaching levels of analysis and introspection that earlier technologies couldn’t achieve.

As you’d expect in a bazaar, JavaScript offers an absurd number of task runners to choose from. The first, born in 2010, was Jake, shortly after the first release of Node.js itself. Soon after came Grunt, then Brunch, Mimosa, Gulp, Broccoli... Absurd? Crazy? You betcha.

To keep a minimum level of sanity, let’s stick to the three contenders that currently seem to be the most popular.

Analysis

##### Grunt

* **Homepage:** http://gruntjs.com/
* **Numero di task disponibili:** 3.989
* **Data di creazione:** Settembre 2011
* **Github stars:** ★ 8.929
##### Gulp

* **Homepage:** http://gulpjs.com
* **Numero di task disponibili:** 1.136
* **Data di creazione:** Luglio 2013
* **Github stars:** ★ 10.657
##### Broccoli

* **Homepage:** https://github.com/broccolijs/broccoli
* **Numero di task disponibili:** ~200
* **Data di creazione:** Maggio 2013
* **Github stars:** ★ 1.787

As the stats show, Grunt was the first “second-generation” task runner to arrive, and that’s exactly why it has the largest database of plugins and tasks. Gulp, on the other hand, is the most followed and supported system right now. Broccoli was mentioned because it’s relatively popular, but even though it was born around the same time as Gulp, it still hasn’t taken off: it has an order of magnitude fewer tasks available than the other two, so we dropped it from the competition because the gap was too wide.

So back to the survivors, Grunt and Gulp. The two differ sharply both in philosophy and in how tasks are written.

Grunt’s motto is “configuration over code”... here’s what a typical Gruntfile.js looks like (the equivalent of the Rakefile in the Ruby world, so to speak) when you want to first concatenate and then minify a set of JavaScript files:

// Gruntfile.js
module.exports = function(grunt) {
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');

  grunt.initConfig({
    concat: {
      scripts: {
        src: ['src/**/*.js'],
        dest: 'temp/all.js'
      }
    },
    uglify: {
      scripts: {
        src: 'temp/all.js',
        dest: 'build/all.js'
      }
    },
  });

  grunt.registerTask('default', ['concat:scripts', 'uglify:scripts']);
};

As you can see, we have two tasks: the first concatenates the source files and writes them to a temporary file; the second takes the temporary file, minifies it and saves the result to the final destination.

Even from this trivial example, we can see that in fact not a single line of code was written to set up the tasks: all we did was pass the parameters needed to configure the two tasks as a hash to the grunt.initConfig() method.

Let’s compare it with the equivalent gulpfile.js:

// gulpfile.js
var gulp   = require('gulp');
var uglify = require('gulp-uglify');
var concat = require('gulp-concat');

gulp.task('default', function() {
  return gulp
         .src('src/**/*.js')
         .pipe(concat('all.js'))
         .pipe(uglify())
         .pipe(gulp.dest('build/'));
});

Simplistic as the example is, we can already spot some fundamental differences between the two approaches:

  • Gulp takes advantage of one of Node.js’s strengths: streams. Unlike Grunt, instead of generating temporary files at every intermediate step, the various file transformations are piped into one another. There are obvious performance gains, but perhaps the most important point is that we no longer have to worry about naming these temporary files, or deleting them once the task is done. One less problem, which makes the code much easier to understand.
  • Gulp tasks aren’t configured, they’re programmed. We import Gulp plugins with idiomatic Node.js require() calls, and the “body” of the task is written in JavaScript. Unlike with Grunt, if we wanted to add an if inside the task to decide at runtime whether to pipe in a given task or not, we’d be completely free to do so. Want to reuse part of the pipeline in a second task? Just refactor the code with method extraction.
  • With Grunt, as the number of tasks grows, it becomes extremely hard to follow how the various subtasks chain together. It’s a very unfortunate consequence of Grunt’s choice to group the configuration hash by plugin rather than by task. This choice completely breaks the principle of locality, forcing us to keep jumping from one part of the file to another to piece a task back together.

Our pick

After reading the analysis, our choice is probably obvious: Gulp [^bemo].

[^bemo]: BEMO, our frontend project starter, sadly includes a Grunt plugin... youthful mistakes :) We’ll switch to an equivalent Gulp plugin soon; in the meantime you can use gulp-grunt to bridge the two systems.

We have used both task runners on different projects, and while Grunt proved extremely solid, on medium-complexity projects it led to configuration hashes of more than 500 lines, practically impossible to manage given the number of temporary files to generate at intermediate steps and the context switches it forced on us.

There are Grunt plugins, which we have used successfully, such as load-grunt-configs that at least improve things by splitting the configuration across multiple files... but Grunt’s typical grouping by plugin remains unchanged and unsolved.

With the same tasks configured, Gulp let us dramatically reduce the length and complexity of the code, and therefore the effort to maintain it over time.

Coming next: module loaders and package managers :)

With task runners we have really only scratched the outer layer of the magical world of single-page applications... next week we’ll sneak into its innermost corners with a roundup of the main module loading mechanisms (AMD, CommonJS), along with their libraries and package managers (npm, Bower).

Follow us on Twitter or via RSS feed to stay up to date on the next installment!

footer