Grunt vs Gulp: Battle of the Build Tools

#

In JavaScript land, things move fast and technology advances at a crazy pace. Most modern applications are built using technologies like Vue.js and React, making use of module bundlers like Webpack. Webpack does a lot for you and handles things like bundling, minification and source maps automatically (or through the use of plugins) making old-school task runners like Grunt and Gulp redundant. Or so you might think.

In reality, however, many projects don’t require a full-blown module bundler like Webpack but still need a way to compile their front-end assets. Webpack wasn’t designed to be a task runner and so it isn’t easy to configure it to build assets without bundling modules. So task runners like Grunt and Gulp still have their place and we still use both here at Delicious Brains as build tools for different products we develop.

In this article, we’re going to look at both Grunt and Gulp to see what their differences are and look at the pros and cons of using each as a build tool for front-end assets.

How are Grunt and Gulp Different?

Before we look at how these build tools are different it’s worth noting that they are both designed to do the same thing: automate tasks that you would otherwise have to do manually. These tasks can include:

  • Compiling Less/Sass to CSS
  • Concatenating and minifying CSS and JavaScript
  • Linting code
  • Optimizing images
  • Running tests
  • Much more…

Both of these tools will help you automate the compiling of your front-end assets and make your builds more consistent and reliable, mainly through the use of plugins contributed by the community.

Grunt is probably the more intuitive of the two build tools as it works by defining tasks in a single configuration file called Gruntfile.js. You can specify the configuration for each task and Grunt will run each task in sequence. This makes Grunt files easy to read and understand.

Gulp, on the other hand, focuses on the “code over configuration” principle by making each task definition a JavaScript function. There is no configuration involved up-front (although functions can normally take configuration values) and you can then chain functions together to create your build script. One big advantage of doing this is that Gulp can make use of node streams, meaning that Gulp doesn’t have to write intermediary files to disk. The upshot of this is that Gulp is usually much faster than Grunt, although it can have a slightly steeper learning curve because of the use of streams and promises.

Build Scripts

One of the easiest ways to compare Grunt vs Gulp is to create the same build script using each tool and comparing the configuration files and performance. So let’s do that.

We’ll create a Gruntfile.js and a Gulpfile.js that both do the following:

  • Compile Sass to CSS
  • Concatenate and minifying CSS and JavaScript
  • Optimize images

Before we begin the steps to create the build files I should point out that I’ve created a GitHub repo as a demo of what we are testing here with all of the necessary files and some demo content if you want to check it out.

First, make sure we have both the Grunt and Gulp CLIs installed on our machine so we can run them:

npm install -g grunt-cli gulp-cli

Next, let’s install the packages we require for both Grunt and Gulp to be run. You can do this manually by running npm install {package} --save-dev or by copying the package.json from the repo and just running npm install:

{
    "name": "grunt-vs-gulp",
    "version": "1.0.0",
    "devDependencies": {
        "grunt": "^1.0.1",
        "grunt-contrib-concat": "^1.0.1",
        "grunt-contrib-cssmin": "^2.2.1",
        "grunt-contrib-imagemin": "^2.0.1",
        "grunt-contrib-uglify": "^3.1.0",
        "grunt-sass": "^2.0.0",
        "gulp": "^3.9.1",
        "gulp-clean-css": "^3.9.0",
        "gulp-concat": "^2.6.1",
        "gulp-imagemin": "^3.3.0",
        "gulp-sass": "^3.1.0",
        "gulp-uglify": "^3.0.0"
    }
}

For Grunt, we need to create a Gruntfile.js in the root of our project. It looks like this:

module.exports = function (grunt) {

    grunt.initConfig({
        sass: {
            dist: {
                files: {
                    'dist-grunt/css/style.css': 'assets/scss/style.scss'
                }
            }
        },
        concat: {
            css: {
                files: {
                    'dist-grunt/css/styles.css': [
                        'dist-grunt/css/style.css',
                        'assets/css/test.css'
                    ],
                },
            },
            js: {
                files: {
                    'dist-grunt/js/scripts.js': [
                        'assets/js/test1.js',
                        'assets/js/test2.js'
                    ],
                },
            },
        },
        cssmin: {
            dist: {
                files: {
                    'dist-grunt/css/styles.min.css': ['dist-grunt/css/styles.css']
                }
            }
        },
        uglify: {
            dist: {
                files: {
                    'dist-grunt/js/scripts.min.js': ['dist-grunt/js/scripts.js']
                }
            }
        },
        imagemin: {
            dynamic: {
                files: [{
                    expand: true,
                    cwd: 'assets/img/',
                    src: ['**/*.{png,jpg,gif}'],
                    dest: 'dist-grunt/img'
                }]
            }
        }
    });

    grunt.loadNpmTasks('grunt-sass');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-imagemin');

    grunt.registerTask('default', ['sass', 'concat', 'cssmin', 'uglify', 'imagemin']);

};

As you can see the configuration of Grunt tasks isn’t very expressive but it is fairly easy to understand. Also because Grunt runs tasks in sequence we can make safe assumptions about things like files being available (e.g. dist-grunt/css/style.css has been compiled before running the concat task).

For Gulp we need to create a Gulpfile.js in the root of our project. It looks like this:

var gulp = require('gulp');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var cleanCSS = require('gulp-clean-css');
var uglify = require('gulp-uglify');
var imagemin = require('gulp-imagemin');

gulp.task('sass', function () {
    return gulp.src('assets/scss/**/*.scss')
        .pipe(sass().on('error', sass.logError))
        .pipe(gulp.dest('dist-gulp/css'));
});

gulp.task('css', ['sass'], function () {
    return gulp.src([
            'dist-gulp/css/style.css',
            'assets/css/test.css'
        ])
        .pipe(concat('styles.min.css'))
        .pipe(cleanCSS())
        .pipe(gulp.dest('dist-gulp/css'));
});

gulp.task('js', function () {
    return gulp.src('assets/js/**/*.js')
        .pipe(concat('scripts.min.js'))
        .pipe(uglify())
        .pipe(gulp.dest('dist-gulp/js'));
});

gulp.task('img', function () {
    return gulp.src('assets/img/*')
        .pipe(imagemin())
        .pipe(gulp.dest('dist-gulp/img'));
});

gulp.task('default', ['css', 'js', 'img']);

As you can see it is much more expressive and allows us to group things in a more logical manner (e.g., grouping all CSS/JS related tasks). Also because of Gulp’s streams implementation, each task can be run in parallel which is part of the reason it is much faster than Grunt.

However, you do have to be careful with this as you need to make sure you don’t break any dependency chains. For example, here we require dist-gulp/css/style.css to exist in the css task, so we need to make sass a prerequisite of the css task to make sure it is run in sequence.

Performance

So now that we have both of these build scripts up-and-running, how do they actually perform?

Grunt
Grunt performance

Gulp
Gulp performance

  • Grunt: 1.6 secs
  • Gulp: 0.59 secs

These results aren’t surprising (as we expected Gulp to be faster) but it does go to show just how much faster Gulp is over Grunt. It’s worth noting that while imagemin took up a big percentage of the process time for both scripts, it seems to run much slower on Grunt but with better compression results. My guess is that this is probably caused by a different default config.

On a small project like ours, a difference of 1 second might not be a big deal. However, for bigger projects that speed difference is multiplied and Grunt can end up being much, much slower in total.

Verdict

So should you use Grunt or Gulp? As with all of these things, there isn’t really a “right” or “wrong” answer. It depends on the context and what you’re comfortable with. As a summary here are some pros and cons for each tool:

Grunt

Pros

  • Easy to learn and configure
  • Mature community with lots of plugins

Cons

  • Can become brittle for larger projects
  • Slower than Gulp

Gulp

Pros

  • Much faster because of the use of streams
  • More flexible due to expressive nature

Cons

  • Steeper learning curve

Personally, I tend to go to Gulp as my first port of call these days, but that doesn’t mean that Gulp is necessarily a better option. The Grunt community is still going strong and both tools look like they’re going to be around for a while yet.

I should mention that another up and coming alternative to task runners like Grunt and Gulp is simply using npm scripts with command line tools. Instead of wrapping tools in plugins to make them work the Grunt/Gulp way, you simply run the tools themselves and use npm scripts to create a build chain. If you’d like to learn more about using npm scripts let us know in the comments. (Check out my follow-up post on using npm scripts as a build tool)

Do you use a task runner like Grunt or Gulp to compile your front-end assets? Do you prefer Grunt/Gulp, if so why? Got any good build tool tips? Let us know in the comments.

About the Author

Gilbert Pellegrom

Gilbert loves to build software. From jQuery scripts to WordPress plugins to full blown SaaS apps, Gilbert has been creating elegant software his whole career. Probably most famous for creating the Nivo Slider.