A JavaScript Build System Shootout: Grunt vs. Gulp vs. NPM

Nicolas Bevacqua dives into the task of choosing a JavaScript Build System. He compares the big three: Grunt, Gulp and NPM and discusses the benefits and drawbacks of each.

By Nicolas Bevacqua

Deciding on a technology is always hard. You don’t want to make commitments you won’t be able to back out of, but eventually you’ll have to make a choice and go for something that does what you need it to do. Committing to a build technology is no different in this regard: it’s an important choice and you should treat it as such. Let’s take Grunt for example.

  • Grunt has a healthy community around it, even on Windows
  • It isn’t used almost exclusively by the Node community
  • It’s easy to learn, you just pick plugins and configure them
  • There’s no advanced concepts nor lots of prior knowledge needed

These are all good reasons to use Grunt to teach build processes, but I want to make it clear that I don’t think Grunt is the single best option out there, and that there are other popular build tools that might fit your needs better than Grunt will.

I wrote this brief article to help you understand the differences between Grunt, Gulp, and npm, which are the three build tools that I use most often in front-end development workflows. While we’re at it, I’ll lay out the situations in which a particular tool might be better than the others.

As a first step, let’s discuss where Grunt excels at.

Grunt: The Good Parts

The single best aspect of Grunt is its ease of use. It enables programmers to develop build flows using JavaScript almost effortlessly. All that’s required is searching for the appropriate plugin, reading its documentation, and then installing and configuring it. This ease of use means that members of large development teams, who are often of varying skill levels, don’t have any trouble tweaking the build flow to meet the latest needs of the project. The team doesn’t need to be fluent in Node either, they need to add properties to the configuration object, and task names to the different arrays that make up the build flow.

There’s a plugin base large enough that you’ll rarely find yourself needing to develop your own build tasks, which also enables you and your team to rapidly develop a build process, which is crucial if you’re going for a Build First approach, even when taking small steps and progressively developing your build flows.

It’s also feasible to manage deployments through Grunt, as many packages exist to accommodate for those tasks, such as grunt-git, grunt-rsync, or grunt-ec2, to name a few.

So where does Grunt fall short? It may get too verbose if you have a significantly large build flow. It’s often hard to make sense of the build flow as a whole once it has been in development for a while. Once the task count in your build flows gets to the double digits, it’s almost guaranteed that you’ll find yourself having to run targets which belong to the same task individually, so that you’re able to compose the flow in the right order. Since tasks are configured declaratively, you’ll also have a hard time figuring out the order in which tasks get executed.

Besides that, your team should be dedicated to writing maintainable code when it comes to your builds as well, and in the case of Grunt that means maintaining separate files for the configuration of each task, or at least for each of the build flows that your team uses.

Now that we’ve identified the good and the bad in Grunt, as well as the situations in which it might be a better fit for your project, let’s talk about npm, how it can be leveraged as a build tool, and its differences with Grunt.

npm as a build tool

In order to use npm as a build tool, you’ll need a package.json file and npm itself. Defining tasks for npm is as easy as adding properties to a scripts object in your package manifest. The name of the property will be used as the task name, and the value will be the command you want to execute. The example shown below uses the JSHint command-line interface to run a linter through our JavaScript files and check for errors. You can run any shell command that you need.

{
  "scripts": {
    "test": "jshint . --exclude node_modules"
  },
  "devDependencies": {
    "jshint": "^2.5.1"
  }
}

Once the task is defined, it can be executed in your command-line by running the following command.

npm run test

Note that npm provides shortcuts for specific task names. In the case of test, you could simply do npm test and omit the run verb. You can compose build flows by chaining npm run commands together in your script declarations.

{
  "scripts": {
    "lint": "jshint . --exclude node_modules",
    "unit": "tape test/*",
    "test": "npm run lint && npm run unit"
  },
  "devDependencies": {
    "jshint": "^2.5.1",
    "tape": "~2.10.2"
  }
}

You could also schedule tasks as background jobs, making them asynchronous. Suppose we have the following package file, where we’ll just copy a directory in our JavaScript build flow, and compile an Stylus stylesheet during our CSS build flow (Stylus is a CSS preprocessor). In this case, running the tasks asynchronously is ideal. You can achieve that using & as a separator, or after a command.

{
  "scripts": {
    "build-js": "cp -r src/js/vendor bin/js",
    "build-css": "stylus src/css/all.styl -o bin/css",
    "build": "npm run build-js & npm run build-css"
  },
  "devDependencies": {
    "stylus": "^0.45.0"
  }
}

To learn more about npm as a build tool, you should try and learn how to write Bash commands instead.

Installing npm task dependencies

The JSHint CLI is not necessarily available in your system, and there’s two ways you could go about installing it. If you’re looking to use the tool directly from your command-line, and not in an npm run task, then you should install it globally, using the -g flag as shown below.

npm install -g jshint

However, if you’re using the package in an npm run task, then you should be adding it as a devDependency, as shown below. That’ll allow npm to find the JSHint package on any system where the package dependencies are installed, rather than expecting the environment to have JSHint installed globally. This applies to any CLI tools that aren’t readily available in operating systems.

npm install --save-dev jshint

You aren’t limited to using just CLI tools. In fact, npm is able to run any shell script. Let’s dig into that!

Using shell scripts in npm tasks

Below is an example script that runs on Node, and displays a random emoji string. The first line tells the environment that the script is in Node.

#!/usr/bin/env node

var emoji = require('emoji-random');
var emo = emoji.random();

console.log(emo);

If you were to place that script in a file named emoji at the root of our project, you’d have to declare emoji-random as a dependecy and add the command to the scripts object in the package manifest.

{
  "scripts": {
    "emoji": "./emoji"
  },
  "devDependencies": {
    "emoji-random": "^0.1.2"
  }
}

Once that’s out of the way, running the command is merely a matter of invoking npm run emoji in your terminal.

The good and the bad

Using npm as a build tool has several advantages over Grunt. You aren’t constrained to Grunt plugins, and thus you can take advantage of all of npm, which hosts tens of thousands of packages. You won’t need any additional CLI tooling or files other than npm, which you are already using to manage dependencies, and your package.json manifest, where dependencies and your build commands are listed. Since npm runs CLI tools and Bash commands directly, it’ll perform way better than Grunt could.

Take into account that one of the biggest disadvantages of Grunt is the fact that it’s I/O bound. This means that most Grunt tasks read from disk, and then write to disk. If you have several tasks working on the same files, then chances are the file is going to be read from disk multiple times. In Bash, commands can pipe the output of a command directly into the next one, avoiding the extra I/O overhead in Grunt.

Probably the biggest disadvantage to npm is the fact that Bash doesn’t play all that well with Windows environments. This means that open-source projects using npm run might run into issues when people try to fiddle with them on Windows. In a similar light, it also means that Windows developers will try and use alternatives to npm, instead. That drawback pretty much rules out npm for projects that need to be able to run on Windows.

Gulp, another build tool, presents similarities with both Grunt and npm, as you’ll discover in a moment.

Gulp, the streaming build tool

Gulp is similar to Grunt in that it relies on plugins and it’s cross-platform, supporting Windows users as well. Gulp is a code-driven build tool, in contrast with Grunt’s declarative approach to task definition, making your task definitions a bit easier to read. Gulp is also similar to npm run in that it uses Node streams to read files and pipe data through functions that transform it into output that will end up being written to disk. This means that Gulp doesn’t have the disk-intensive I/O issues that you may observe in using Grunt. It’s also faster than Grunt for the same reason, less time spent in I/O.

The main disadvantage in using Gulp is that it relies heavily on streams, pipes, and asynchronous code. Don’t get me wrong: if you’re into Node, then that’s definitely an advantage as well. But the issue with those concepts is that unless you and your team are well-versed in Node, you’re probably going to run into issues dealing with streams, especially if you have to build your own Gulp task plugins.

When working in teams, Gulp is not as prohibitive as npm, because most of your front-end team probably knows JavaScript, while chances are they’re not that fluent in Bash scripting, and some of them may be using Windows! That’s why I usually suggest to keep npm run to your personal projects, maybe use Gulp in projects where the team is comfy with Node, and Grunt everywhere else. Of course, that’s my personal appreciation, you should think for yourself and figure out what works best for you and your team. Also, you shouldn’t constrain yourself to Grunt, Gulp, or npm run just because those are the tools that work for me. Try and do a little research, and maybe you’ll find a tool that you like even better than those three.

Let’s walk through some examples to get a feel of how Gulp tasks look like.

Running tests in Gulp

Gulp is quite similar to Grunt in its conventions. In Grunt there’s a Gruntfile.js file, used to define your build tasks, and in Gulp the file needs to be named Gulpfile.js instead. The other minor difference is that in the case of Gulp, the CLI is contained in the same package as the task runner, so you’ll have to install the gulp package from npm both locally and globally.

touch Gulpfile.js
npm install -g gulp
npm install --save-dev gulp

To get started, I’ll create a Gulp task to lint a JavaScript file, using JSHint just like you’ve already seen with Grunt and npm run. In the case of Gulp, you’ll have to install the gulp-jshint Gulp plugin for JSHint.

npm install --save-dev gulp-jshint

Now that you’re fully equipped with the CLI, that you globally installed, the local gulp instalation, and the gulp-jshint plugin, you can put together the build task to run the linter. To define build tasks with Gulp, you’ll have to write them programatically in the Gulpfile.js file.

First off you’ll use gulp.task passing it a task name and a function. The function contains all of the code necessary to run that task. Here you should use gulp.src to create a read stream into your source files, using a globbing pattern like the ones you’ve seen in our experiences learning about Grunt. That same stream should be piped into the JSHint plugin, which you can configure or just use with the defaults it comes with. Then all you’d have to do is pipe the results of the JSHint task through a reporter, and have it print the results to your terminal. All of what I’ve just described results in the Gulpfile presented below.

var gulp = require('gulp');
var jshint = require('gulp-jshint');

gulp.task('test', function () {
  return gulp
    .src('./sample.js')
    .pipe(jshint())
    .pipe(jshint.reporter('default'));
});

I should also mention that I’m returning the stream so that Gulp understands that it should wait for the data to stop flowing before it considers the task to be completed. You could use a custom JSHint reporter in order to have the output be a bit more concise, and thus easier to read by humans. JSHint reporters don’t need to be Gulp plugins, so you could use jshint-stylish for example. Let’s install it locally.

npm install --save-dev jshint-stylish

The updated Gulpfile should look as shown below. It’ll load the jshint-stylish module to format the reporting output.

var gulp = require('gulp');
var jshint = require('gulp-jshint');

gulp.task('test', function () {
  return gulp
    .src('./sample.js')
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

You’re done! That’s all there is to declaring a Gulp task named test. It can be run using the command below, provided that you installed the gulp CLI globally.

gulp test

That was quite a trivial example. Just as well as ou can pipe the output of the JSHint linter through a reporter that will print the results of the linting test, you could also write output to disk by using gulp.dest, which creates a write stream. Let’s step through another build task.

Building a library in Gulp

To get started, let’s do the bare minimum: read from disk with gulp.src and write back to disk piping the contents of the source file into gulp.dest, effectively just copying the file into another directory.

var gulp = require('gulp');

gulp.task('build', function () {
  return gulp
    .src('./sample.js')
    .pipe(gulp.dest('./build'));
});

Copying the file is nice, but it doesn’t quite minify its contents. To do that, you’ll have to use a Gulp plugin. In this case you could use gulp-uglify, a plugin for the popular UglifyJS minifier.

var gulp = require('gulp');
var uglify = require('gulp-uglify');

gulp.task('build', function () {
  return gulp
    .src('./sample.js')
    .pipe(uglify())
    .pipe(gulp.dest('./build'));
});

As you’ve probably realized, streams enable you to add more plugins while only reading and writing to disk once. As an example, let’s pipe through gulp-size as well, which will calculate the size of the contents that are in the buffer, and print that to the terminal. Note that if you add it before Uglify then you’d get the unminified size, and if you add it after, you’ll get the minified size. You could also do both!

var gulp = require('gulp');
var uglify = require('gulp-uglify');
var size = require('gulp-size');

gulp.task('build', function () {
  return gulp
    .src('./sample.js')
    .pipe(uglify())
    .pipe(size())
    .pipe(gulp.dest('./build'));
});

Just to reinforce the point on composability, being able to add or remove pipes as needed, let’s add one last plugin. This time I’ll use gulp-header to add some license information to the minified piece of code, such as the name, the package version, and the license type.

var gulp = require('gulp');
var uglify = require('gulp-uglify');
var size = require('gulp-size');
var header = require('gulp-header');
var pkg = require('./package.json');
var info = '// @v, n';

gulp.task('build', function () {
  return gulp
    .src('./sample.js')
    .pipe(uglify())
    .pipe(header(info, { pkg : pkg }))
    .pipe(size())
    .pipe(gulp.dest('./build'));
});

Just like in Grunt, in Gulp you can define flows by passing in an array of task names to gulp.task, instead of a function. The main difference between Grunt and Gulp in this regard is that Gulp will execute these dependencies asynchronously, while Grunt executes them synchronously.

gulp.task('build', ['build-js', 'build-css']);

In Gulp, if you want to run tasks synchronously you’ll have to declare a task as a dependency and then define your own task. All dependencies are executed before your task starts.

gulp.task('build', ['dep'], function () {
  // here goes the task that depends on 'dep'
});

If you take anything away from this article, have that be this quote.

It doesn’t matter which tool you use, as long as it allows you to compose the build flows you need in a way that doesn’t make you work too hard for it.

This article originally appeared on Nicolas’ blog, PonyFoo, here.

Previous

5 things you won’t believe are only built with CSS

Creating An Application With Sails.js, Angular.js and Require.js Part 2 – Adding Dependencies

Next

23 thoughts on “A JavaScript Build System Shootout: Grunt vs. Gulp vs. NPM”

  1. Interesting you take Gulp as being the more complicated one since it uses Node streaming. I’ve always heard Gulp described as the less complicated one of the bunch.

    • That’s until you try to develop a decent quality Gulp task plugin, or troubleshoot an existing plugin into doing what you actually want.

    • Well, Grunt is just JSON, and often easy to look at, but not to setup. Gulp may be the opposite, it might take longer to look at and understand, but the natural code flow is far easier to manage and develop with – at least from my experience.

  2. I’ve found Gulp much easier to use, especially in situations where you might be running a stream through 4 tasks or more (sass!). Dealing with concurrency gets quite confusing in a complex build process though.

    • If you ever work on a significantly large project, need to run unit tests, do robust deployments, or especially: work with others on a team, then you definitely need tools like these. Without them, you’ll waste an inordinate amount of time on the things I just mentioned, when you could get real work done by automating it.

  3. This is all a bit academic and doesn’t really address the big difference between Gulp & Grunt that really has an impact on your workflow – yes, Gulp will often be substantially faster, but Grunt’s main disadvantage is its inflexibility, and the fact that disk I/O means tasks have to be separate and yet aware of where their target files (and where they should be dumped).

    The reason I and others I know have moved to Gulp is that you can put transformations in streams. A plugin doesn’t have to read from a specific location, transform files, and dump them back in another location (for other files to do the same): you can grab a whole bunch of files, and pass them through a series of transformations bit by bit without any of the intervening transformations needing to be aware of each other. This allows you to run expensive operations like Ruby SASS & Browserify compilations in parallel – and introduce other processes (hinting / validation, Coffeescript transpilation, macros, prefixing, minification, sourcemaps) at whatever point you want on the streams. All this can happen in memory, until you finally decide to dump the contents of the streams back onto the hard disk. Combine this with something like Browsersync and your development workflow can become incredibly powerful and superfast.

    BUT streams are complicated. Sourcemaps, Browserification, and other tasks that can potentially turn multiple files into multiple other files, have difficulty working well with streams. Continuous asynchronous processes need careful resolution and error handling, and these often break. Until version 4 (some way off), there are many seemingly trivial things in Gulp which are a massive headache to do properly.

    My advice would be to stick to use Grunt until you decide it’s not flexible or fast enough for you, and then /try/ to migrate your workflow to Gulp. How much you like the idea of streams and how much faster you want your build step to be are ultimately trivial concerns compared to the real deciding factor in which build tool to pick: can it do what you want it to?

  4. This is a great article…I had never considered using NPM as a build tool. I’ve worked extensively with both Grunt and Gulp and have to say that Grunt has a much more robust API, especially when building custom plugins. That said, I find Gulp much more enjoyable to work with, especially when breaking tasks into separate files as demonstrated here https://viget.com/extend/gulp-browserify-starter-faq. The same can be done with Grunt, and the ability added to run tasks Async https://dontkry.com/posts/code/grunt-tricks.html, https://dontkry.com/posts/code/grunt-tricks2.html.

    The main problem I’ve run into with Grunt is due to the streaming nature it is very difficult or potentially impossible at this time to dynamically generate multiple dest files, which is generally trivial in Grunt. For example in Grunt I may have this ‘pages’ target for compiling handlebars templates to HTML using Assemble.io:

    pages: {
    files: [
    {
    src: [‘**/*.hbs’],
    dest: ‘/’,
    cwd: ‘/’,
    expand: true
    }
    ]
    }

    This will easily scaffold a directory tree in the destination directory that matches my tree in the pre-compiled directory.

    Alternatively, I have not yet found a way to mimic this behavior in Gulp, which seems to be a know bug/deficency, and to even come close it seems I would have to specify multiple Gulp tasks. The same task above would look something like this in Gulp:

    gulp.task(‘assemble’, function () {
    gulp.src(‘templates/pages/**/*.hbs’)
    .pipe(assemble(options))
    .pipe(gulp.dest(‘dist/’));
    });

    But the result would be all of my files in the potentially nested ‘pages’ directory structure, being output by their filename into the root of the ‘dist’ directory. If anyone has information on how to accomplish such a task I would greatly appreciate the feedback

    • The above post was formatted by the editor. The Grunt task should be something like:

      dest: ‘dist/’,
      cwd: ‘public/’

  5. The bottom line, and something not addressed in this article, is that pick SOMETHING that works for your organization. The advantage to a configuration driven system like Grunt is that it’s easy for people unfamiliar with Javascript to be productive.

    Gulp is “the new hotness”, but we’ve been using Grunt for several months on several projects with great success. Builds aren’t as fast as they could be, but they’re fast enough for us to not notice them and slow our team down during development.

  6. Nice to see JS people reinventing the wheel. Grunt reinvented the Unix shell replacing scripts with plugins and now Gulp has reinvented the Unix pipe too! Seriously, it’s cmd1 | cmd2 | cmd3 — the shell is a fucking streaming task (=script) runner.

Comments are closed.