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.

We have a complete series on how to develop a WordPress plugin using Webpack & React. 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 take a 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.

Gulp vs Grunt: How are they 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 unit 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. At the time of writing this article, the Grunt plugin registry contained 6,250 plugins, whereas the Gulp plugin registry contained 4000+ different plugins. A considerable amount for both, although Grunt takes the lead here.

How do Grunt and Gulp work?

Grunt works by defining tasks in a file called Gruntfile.js, structured in a very similar way to JSON. You can specify the configuration for each task and Grunt will run those in sequence. This file is easy to read and understand, making Grunt probably the more intuitive of the two build tools.

Gulp, on the other hand, focuses on the “code over configuration” principle by making each task definition a JavaScript function declared on a file called Gulpfile.js. Gulp code is often much shorter than Grunt’s because there is no configuration involved up-front (although functions can normally take configuration values). You can then chain functions together, running in parallel or in series, to create your building script.

One big advantage 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.

Building the Scripts

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 file 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.1",
   "devDependencies": {
      "grunt": "^1.1.0",
      "grunt-contrib-concat": "^1.0.1",
      "grunt-contrib-cssmin": "^3.0.0",
      "grunt-contrib-imagemin": "^4.0.0",
      "grunt-contrib-uglify": "^4.0.1",
      "grunt-sass": "^3.1.0",
      "gulp": "^4.0.2",
      "gulp-clean-css": "^4.3.0",
      "gulp-concat": "^2.6.1",
      "gulp-imagemin": "^7.1.0",
      "gulp-sass": "^4.1.0",
      "gulp-stats": "0.0.4",
      "gulp-uglify": "^3.0.2",
      "node-sass": "^4.14.1",
      "time-grunt": "^2.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) {
   const sass = require('node-sass');
   const mozjpeg = require('imagemin-mozjpeg');

   require('time-grunt')(grunt);

   grunt.initConfig({
      sass: {
         options: {
            implementation: 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: {
            options: {
               optimizationLevel: 5,
               use: [mozjpeg()]
            },
            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 a 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).

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

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

sass.compiler = require('node-sass');

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

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

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

function img() {
   return gulp.src('assets/img/*')
      .pipe(imagemin([
         imagemin.mozjpeg({optimizationLevel: 5})
      ]))
      .pipe(gulp.dest('dist-gulp/img'));
}

exports.default = gulp.parallel(gulp.series(scss, 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. To prevent this, you can define tasks to be run in series as we did with the scss and css tasks. Doing it this way, we can be certain that dist-gulp/css/style.css is already compiled when the css task needs it.

Performance

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

Grunt
Grunt performance results

Gulp
Gulp performance results

  • Grunt Tasks: 1.6 secs
  • Gulp Tasks: 0.84 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.

On a small project like ours, a difference of ~800 milliseconds 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 a “right” or “wrong” answer – it really comes down to personal preference. 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.

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 tooltips? 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.