8 ways to improve your Grunt set-up

Mark McDonnell and Tom Maslen of BBC News talk you through eight steps to help keep your Grunt set-up fast, maintainable and scalable.

Grunt – which won Open Source Project of the Year at May's net Awards 2014 – has quickly become an essential configuration-based command line tool within our industry for running tasks that can handle all kind of requirements. The BBC News development team use Grunt on a daily basis to make sure the bbc.co.uk/news codebase is tested, linted, formalised, optimised and automated.

Here, the BBC's Mark McDonnell and Tom Maslen of BBC News talk through eight ways to help keep your Grunt set-up fast, maintainable and scalable.

01. Keep your Gruntfile maintainable

One of the biggest concerns for developers working with Grunt is that this wonderfully powerful configuration file can evolve into an unwieldy monster. As with most complex tools, problems with maintainability can rapidly accumulate in a short period of time; leaving users overwhelmed with how best to resolve the complexity they're now faced with.

The best way to tackle this problem is to simplify as much as you can. If we were to take a leaf out of the object-oriented design handbook, we would know that our configuration file is doing too much and that we need to break it down into component parts to ease our ability to extend and manage our Gruntfile requirements (when we need to add more tasks and configuration settings, for example).

What we need to do is simplify our Gruntfile's structure. There are a few ways to do this, but the majority of solutions you read about boil down to different implementations of that general theme. The example I'm going to demonstrate is the best way possible to reduce the size and complexity of your Gruntfile.

In your root directory (where you have your Gruntfile) you'll create a 'grunt' folder. Inside that folder will be individual JavaScript files; each containing a different task that you would have included within your main Gruntfile.

Your directory structure could look something like the following...

|— Gruntfile
|— package.json
|— grunt
|           – contrib-requirejs.js

Our Gruntfile can now be as simple as:

module.exports = function(grunt) {
    grunt.loadTasks('grunt');
};

Isn't that better? It's worth noting that the string 'grunt' that was passed to the loadTasks method has nothing to do with the actual Grunt object; it refers to the name of the folder you created.

You could have called the folder anything: 'omg-so-sexy' , for example, in the above code would be grunt.loadTasks('omg-so-sexy'). With each task in its own file, we need to define the task slightly differently to how it would usually be added in the Gruntfile; contrib-requirejs.js should be structured thus:

module.exports = function(grunt) {
    grunt.config('requirejs', {
        compile: {
            options: {
                baseUrl: './app',
                name: 'main',
                out: './app/release/main.js'
            }
        }
    });
    grunt.loadNpmTasks('grunt-contrib-requirejs');
};

02. Keep that config outside of your config!

Another important technique that we can utilise is to move specific types of configuration outside of the Gruntfile. One obvious place that we see this happen a lot is with the JSHint plug-in, which can be quite large, and so can take up a lot of space within your overall Gruntfile.

Luckily JSHint has a built-in solution to this problem. Let's take a look at how we can use it to clean up our JSHint task.

The first step that we need to take is to create a new file called .jshintrc, and within it put your JSON configuration:

{
    "curly": true,
    ...
}
// This is an example, you should define more than one option!

Then from within your JSHint task (which we'll assume is now safely out of the Gruntfile and within its own separate task file) you can specify the location of the configuration file:

jshint: {
    files: ['./app/**/*.js'],
    options: {
        jshintrc: './grunt/.jshintrc'
    }
}

The same approach can be applied to any configuration data. It so happens that the JSHint task came pre-built with that functionality, and so with other pre-built tasks you may need to dynamically load the config file yourself using the Grunt API.

03. Only run tasks when a change has occurred

If you haven't heard of the grunt-contrib-watch task then it should be the first thing you look at next, because it's a life saver for ensuring you only run a task when the associated files with that task have actually changed.

Imagine you have a JavaScript test suite. You wouldn't want to have to manually run the task after every save of a file (especially if you're doing TDD – test-driven development), because that's a slow workflow. It would be better if you simply saved your JavaScript file … and BOOM the relevant tests are off and running! That's what this task does.

Below you'll find a simple example, which demonstrates how you can create a scripts sub-task off the main watch task, which watches all your JavaScript files – and when any of them have changed, it'll run the jshint task that has been set up separately:

watch: {
  scripts: {
    files: ['app/**/*.js'],
    tasks: ['jshint'],
    options: {
      spawn: false,
    },
  },
}

04. Only run tasks against files that have actually changed

The only thing that's faster than using grunt-contrib-watch to run tasks when a file changes, is to run tasks against only those files that have actually changed since the last time the task was run. This is where the grunt-newer task comes in handy. You define your tasks as normal, and the only thing you need to do is prefix the name of the task you want to run with newer:.

For example:

grunt.initConfig({
    jshint: {
        all: {src: 'src/**/*.js'}
    }
});
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-newer');
grunt.registerTask('lint', ['newer:jshint:all']);

Now when you run grunt lint it will only run the jshint task against files that have changed since the last time the jshint task was run.

So, if you run the task and then edit a single JavaScript file, then when that file is saved that single file will only be linted, because the grunt-newer task knows no other files need to be run against JSHint again.

05. Create a default Grunt set-up starting point for all projects

Grunt has a built-in feature called grunt-init. It lets you define a template project structure that gets dynamically injected with configurable values when you start a new project.

It's a command line tool configured by a JSON file. You set questions in the JSON file, these are answered on the command line, and the values are passed into the project template.

For example, imagine that you develop a large number of Node.js modules, which you publish to NPM (Node Package Manager). Rather than you having to create the same folder structure and documentation README files over and over (but only changing minor details such as the name of the library), you could create a template that grunt-init can utilise to set up everything automatically for you.

06. Understand what each task does

The biggest criticism of Grunt is that it's slow. While Grunt does indeed contain some sub-optimal design decisions (albeit ones that are being actively addressed for the upcoming Grunt v1.0 release), a Grunt set-up overloaded with tasks is obviously going to run slowly.

By way of an example, we recently worked on a project in which we had added a 90Kb data file for D3.js (a popular data visualisation tool) to compile into an interactive map. This data file caused our Grunt build to take over two minutes to render a concatenated JS file (grunt-contrib-requirejs). It's not a great experience being forced to wait that long between saves.

The build took such a long time long because grunt-contrib-requirejs was creating a JavaScript sourcemap for the concatenated file, a fruitless task for a data file with thousands of points. Blacklisting the data file brought the build back down to just a few seconds.

Run Tasks in parallel

A great way to speed up your Grunt running time is to run tasks in parallel. Two very popular tasks can help you do this?

  • grunt-parallel
  • grunt-concurrent

To be honest there isn't much to choose between them – we'd lean slightly towards grunt-concurrent because:

  • The API is slightly more straightforward
  • The project chatter on GitHub is more recent (relying on dead projects isn’t fun)
  • It's made by Sindre Sorhus!!!

Regardless of which one you choose (pick grunt-parallel if you also want to run custom – non Grunt – tasks) the one thing you should do is use it together with the time-grunt plug-in, which is a fantastic tool that tells you how long each task takes to run.

You've probably heard that quote before, but it's true. Before you start micro-optimising every part of your Gruntfile, the first thing you should do is measure how long the build takes to run in its current form. Then, after each refactoring, analyse the build's performance to ensure you've not introduced a regression.

For example, we recently added the grunt-concurrent plug-in into our Grunt set-up; it sped up the processing of two sub tasks with RequireJS, but it actually increased the build time for our Sass tasks. This was because the two sub tasks within Sass were running at 0.8 and 0.2 seconds.

Running them side-by-side with the 0.5 second penalty of spinning up a second instance of Grunt increased the time to 1.3 seconds! This is because there is a cost to running two tasks in parallel: normally about 0.5 seconds, the time it takes to spin up another instance of Grunt.

08. Conditionally load tasks

Grunt loads into memory all the tasks you add to the Gruntfile, regardless of whether or not they are going to be used. With small Grunt set-ups this isn't an issue, but as you add more tasks into your set-up it will take longer for Grunt to spin everything up before running the task you requested. This can be especially painful if you have a task that depends on something particularly heavy, such as GraphicMagick, which can take five seconds to load into memory.

So let's be a bit tricksy and set up the Grunt config in a very specific way in order to get around this problem. We can define tasks within the config whose only role is to define and run other tasks, like this:

module.exports = function (grunt) {
    grunt.registerTask('images', [], function () {
        grunt.config('responsive_images', {
            main: { ... }
        });
        grunt.loadNpmTasks('grunt-responsive-images');
        grunt.task.run('responsive_images');
    });
};

The task images is loaded into memory each time Grunt runs, but the sub-tasks within it are not. These will be loaded and run only if you run grunt images. This will massively decrease the spin-up time Grunt needs before being ready to run a task. The only drawback is that you now have sub layers of tasks, so you'll need to give the tasks names that might describe or get confused with the tasks ran within them.

Conclusion

We hope you've found this article useful. Ultimately, however, the best way to keep your Grunt set-up maintainable, fast and scalable is to understand what you are doing.

Keep reading about Grunt; follow thought leaders such as Ben Altman - the creator of Grunt, Sindre Sorhus – Node.js superstar, and Addy Osmani – workflow enthusiast, as well as @gruntjs for the latest news on the project. The best craft people become experts in how to use their tools.

Words: Mark McDonnell and Tom Maslen

Mark McDonnell is wannabe programming polyglot and Tom Maslen is an expert in JavaScript, HTML and CSS. This article originally appeared in net magazine issue 256.