Optimize Gruntfile

Original author: Paul Bakaus
  • Transfer

Introduction


If Grunt is a new word for you, then you can first read Chris Coyers ' article “Grunt for people who think things like Grunt are ugly and heavy . After the introduction from Chris, you will have your own Grunt project and you will already taste all the opportunities that Grunt provides to us.

In this article, we will focus not on how many plugins for Grunt should be added to your project, but on the process of creating the assembly itself. You will gain practical skills in the following aspects of working with Grunt:

  • How to keep your gruntfile neat and tidy
  • How to greatly improve your build time
  • How to keep abreast of build status


Lyrical digression: Grunt is just one of many devices that you can use to perform your tasks. If Gulp suits you better, super! If, after reviewing the features described in this article, you still want to create your own set of tools for building - no problem! We review Grunt in this article because it is an already established ecosystem with a large number of users.

Organizing Your Gruntfile


If you plug in many Grunt plugins or intend to write many tasks in your Gruntfile, then it will quickly become cumbersome and difficult to support. Fortunately, there are several plugins that specialize in this particular problem: returning your Gruntfile a clean and tidy look.

Gruntfile before optimization


This is what our Gruntfile looks like before optimization:

module.exports = function(grunt) {
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      dist: {
        src: ['src/js/jquery.js','src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
        dest: 'dist/build.js',
      }
    },
    uglify: {
      dist: {
        files: {
          'dist/build.min.js': ['dist/build.js']
        }
      }
    },
    imagemin: {
      options: {
        cache: false
      },
      dist: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.{png,jpg,gif}'],
          dest: 'dist/'
        }]
      }
    }
  });
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-imagemin');
  grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);
};

If now you are going to say “Hey! I thought it would be much worse! It is quite possible to support! ”, Then perhaps you will be right. For simplicity, we added only three plugins without any customization. If this article would give an example of a real Gruntfile, which is used on "combat" projects, we would need endless scrolling. Well, let's see what we can do about it!

Autoload your plugins



Hint: load-grunt-config includes load-grunt-tasks , so if you don’t want to read about it, you can skip this piece, it won’t hurt my feelings.

When you want to add a new plugin to your project, you will have to add it to your package.json as a dependency for your project and then load it into your Gruntfile. For the plugin " grunt-contrib-concat ", it will look like this:

// tell Grunt to load that plugin
grunt.loadNpmTasks('grunt-contrib-concat');

If you remove the plugin using npm and edit your package.json, but forget to update the Gruntfile, your assembly will break. It is here that the small plug-in “load-grunt-tasks” comes to our aid.

Up to this point, we had to manually load our Grunt plugins:

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

With load-gurnt-tasks, you can reduce the amount of code to one line:

require('load-grunt-tasks')(grunt);

After connecting the plugin, it will analyze your package.json, determine the dependency tree of your plugins and load them automatically

Split configuration file


load-grunt-tasks reduces the size and complexity of your Gruntfile, but if you are going to build a large application, the file size will still grow steadily. At this point, a new plugin comes into play: load-grunt-config! It allows you to split the Gruntfile into tasks. Moreover, it encapsulates load-grunt-tasks and its functionality!

Important: Sharing your Gruntfile is not always the best solution. If you have many related settings between tasks (for example, for standardizing Gruntfile), you should be a little more careful.

When using the "load-grunt-config" plugin, your Gruntfile will look like this:

module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

Yes this is true! That's the whole file! But where are the configuration files now?

Create a directory and name it grunt. Let's do it right in the directory where your Gruntfile is. By default, the plugin connects files from this directory by the names specified in the tasks that you are going to use. The structure of our project should look like this:

- myproject/
-- Gruntfile.js
-- grunt/
--- concat.js
--- uglify.js
--- imagemin.js

Now let's set the settings for each of the tasks in separate files (you will see that this is almost a normal copy paste from Gruntfile).

grunt / concat.js

module.exports = {
  dist: {
    src: ['src/js/jquery.js', 'src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
    dest: 'dist/build.js',
  }
};

grunt / uglify.js

module.exports = {
  dist: {
    files: {
      'dist/build.min.js': ['dist/build.js']
    }
  }
};

grunt / imagemin.js

module.exports = {
  options: {
    cache: false
  },
  dist: {
    files: [{
      expand: true,
      cwd: 'src/',
      src: ['**/*.{png,jpg,gif}'],
      dest: 'dist/'
    }]
  }
};

If the JavaScript configuration blocks are not yours, then load-grunt-tasks also allows you to use the YAML or CoffeeScript syntax. Let's write our last needed file using YAML. This will be an aliases file. It is a file in which all task aliases are registered. This is what we must do before calling the registerTask function. Here is the actual file:

grunt / aliases.yaml

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

That's all! Run the following command in the console:

$ grunt

If everything worked, then in fact, we formed a “default” task for Grunt with you. It will run all the plugins in the order we specified in the associations file. Well, now that we have reduced the Gruntfile to three lines of code, we will never have to go into it again to fix the line in any task, this is done. But stop, it still works very slowly! I mean, you have to wait a lot of time before everything gathers. Let's see how we can improve it!

Minimize assembly time


Even though the download speed and startup time of your web application are more important than the time required to build, the slow speed of it can still cause us a lot of inconvenience. This makes the process of automatically assembling the application with plugins, such as grunt-contrib-watch , or building after committing in Git, quite “difficult” - this turns into a real torture. The bottom line is: the faster the assembly takes place, the better and faster your workflow will occur. If your production assembly takes more than 10 minutes, you will resort to it only in extreme cases, and while it will be assembled, you will go to drink coffee. This is a productivity kill. But do not despair, we have something that can make a difference.

Collect only what has changed: grunt-newer


Oh, this feeling, when after assembling the entire project you need to change a couple of files and wait in the second round until everything is assembled again. Let's look at an example when you change one image in the src / img / directory - in this case, running imagemin to perform image optimization makes sense, but only for one image, and, of course, in this case restarting concat and uglify is just waste of precious processor time.

Of course you can always run
$ grunt imagemin
from your console instead of the standard
$ grunt
. This will allow you to run only the task that is necessary, but there is a more rational solution. It is called grunt-newer.

Grunt-newer has a local cache in which it stores information about files that have been changed and runs tasks only for them. Let's see how to connect it.

Remember our aliases.yaml? Let's change

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

on this:

default:
  - 'newer:concat'
  - 'newer:uglify'
  - 'newer:imagemin'

Simply put, we simply add the prefix “newer:” to any of our tasks, which must first be passed through the grunt-newer plugin, which, in turn, will determine for which files to run the task and for which not.

Running tasks in parallel: grunt-concurrent


grunt-concurrent is a plugin that becomes really useful when you have a lot of tasks that are independent of each other and which take a lot of time. It uses the cores of your processor, parallelizing tasks on them.

Especially cool is that the configuration of this plugin is super simple. If you use load-grunt-config, create sl. file:

grunt / concurrent.js

module.exports = {
  first: ['concat'],
  second: ['uglify', 'imagemin']
};

We simply set the parallel execution of the first (first) and second (second) queue. In the first stage, we run only the "concat" task. In the second stage, we run uglify and imagemin, since they are independent of each other, they will be executed in parallel, and therefore, the execution time will be common to both tasks.

We changed the alias of our default task to be processed through the grunt-concurrent plugin. The following is the modified aliases.yaml file:

default:
  - 'concurrent:first'
  - 'concurrent:second'

If you restart the Grunt assembly now, the concurrent plugin will start the concat task first, and then create two threads on different processor cores so that imagemin and uglify work in parallel. Cool!

A quick tip : in our simple example, grunt-concurrent is highly unlikely to make our build noticeably faster. The reason is the overhead (overhead) for running additional threads for Grunt instances. Judging by my calculations, it takes at least 300ms / stream.

How long did the assembly take? Time-grunt comes to the rescue


Well, now we have optimized all our tasks, and it would be very useful for us to know how long it takes to complete each task. Fortunately, there is a plugin that does an excellent job of this task: time-grunt.

time-grunt is not a plugin in the classical sense (it does not connect via loadNpmTask), it rather refers to plugins that you connect "directly", such as load-grunt-config. We will add the connection of this plugin to our Gruntfile in the same way as we already did for load-grunt-config. Now our Gruntfile should look like this:

module.exports = function(grunt) {
  // measures the time each task takes
  require('time-grunt')(grunt);
  // load grunt config
  require('load-grunt-config')(grunt);
};

I apologize for the disappointment, but that's all - try restarting Grunt from your console and for each task (and for the entire assembly) you should see a nicely formatted information about the runtime:



Automatic alert system


Now that you have a well-optimized collector that quickly performs all its tasks and provides you with the option of auto-assembly (i.e. tracking changes to files through the grunt-contrib-watch plugin or using hooks after commits), it would be great to have a system that would be able to alert you when your fresh build is ready to use, or when something goes wrong? Meet grunt-notify .

By default, grunt-notify provides automatic alerts for all errors and warnings that Grunt throws. To do this, he can use any warning system installed on your OS: Growl for Mac OS X or Windows, Mountain Lion's and Mavericks' Notification Center, and, Notify-send. It's amazing that all you need to get this functionality is to install the plugin from the npm repository and connect it to your Gruntfile (remember, if you use grunt-load-config, as described above, this step is automated!).

Here's how the plugin works depending on your operating system:



In addition to errors and warnings, let's configure it to start after our last task is complete.
It is assumed that you are using grunt-contrib-config to separate tasks between files. Here is the file we need:

grunt / notify.js

module.exports = {
  imagemin: {
    options: {
      title: 'Build complete',  // optional
        message: '<%= pkg.name %> build finished successfully.' //required
      }
    }
  }
}

The hash key of our configuration determines the name of the task for which we want to connect grunt-notify. This example will generate an alert immediately after the imagemin task (the last one on the list to complete) is completed.

And in conclusion


If you did everything from the very beginning, as described during the article, now you can consider yourself the proud owner of a super-clean and organized collector, incredibly fast due to parallelization and selective processing. And do not forget that for any assembly result, we will be carefully informed!

If you discover other interesting solutions that will help improve Grunt or, say, useful plugins, please let us know! In the meantime, successful builds!

From translator

I tried to translate this article as close as possible to the original, but in some places I still allowed a couple of frivolities so that it “sounded in Russian”, because not all the turns and methods of constructing English sentences fit well into the Russian language. I really hope that you will treat this with understanding.

I hope you enjoy the article. I am pleased to hear constructive criticism and suggestions for improvement. Comments are also welcome!

Also popular now: