Getting Started with Grunt
Speeding up your site with a build process
Frontend development has changed over the last few years - complexity and sheer size of modern projects has inspired new tooling, build systems and even new languages to streamline our processes. With all this change putting a dev environment together can seem like a daunting task.
The thing is, not putting some kind of build step into even a basic website deployment is a mistake. At the very least you need to optimise your assets as speeding up your website has many benefits. Amazon calculated that a slowdown of just 1 second would cost them $1.6 billion in lost revenue! Having a slow site can even affect your search position as Google take into account page speed when calculating your ranking .
To speed up our site, we need to do several things:
- Concatenate our multiple JavaScript and CSS files into a single js/css file so we only need 1 network request.
- Optimise our image files and minify the HTML, CSS & JavaScript so we're transmitting less bytes.
- Add a revision number to our asset filenames so they can be cached FOREVER!
Doing this by hand would be a pain, but luckily we have a helper - Grunt is a build tool similar to Make or Rake. We can use in our development process to automate all those fiddly tasks and give us a deployable site that's sleek and speedy. Once we've set it up we can use it on all our projects with only a small bit of configuration.
In this article I'll go through building a dev environment from scratch. We'll:
- Modify an existing project to have a build process
- Install & setup our build process tools via Node & npm
- Use Grunt to generate a deployment folder with our optimised assets ready to copy over to our webserver.
If you want to have a look at the finished project it's on Github: basic_grunt_example.
Our Skeleton Setup
As our test subject lets take a simple Bootstrap example who's file structure looks like this
/site
index.html
/css
/fonts
/images
/js
Pretty standard stuff.
To give us somewhere to put the build files lets add that site into a folder, we'll call it basic_grunt_setup
/basic_grunt_setup
/site
Speed Before
Before we start, let's quickly (and un-scientifically) benchmark our current site with Chrome Tools:
6 requests, DOMContentLoaded at 1.09 seconds - I'm sure we can do better than that.
Installing our tools
We're going to be using Grunt for our build process and that's a Node app. What's Node? Good question!
Node.js is JavaScript without the browser. Google's Chrome browser uses a JS engine called V8, back in 2009 Ryan Dahl pulled V8 out of Chrome and packaged it with some library code (for handling http connections, file system access, etc.) and Node.js was born. Although Dahl's original motivation was to use it to build web services it's proved very versatile, you can even use it to control flying robots!
We need to install Node and use it's inbuilt package manager npm to install Grunt itself and the tools we need for our build process.
Installing Node
Head on over to the Node website at http://nodejs.org/ and hit the big install button. This should download the installer for your machine so just run it to get your Node installation up and running.
Hopefully you're experienced with the command line interface to your chosen operating system - if you fancy digging a bit more into Node here's really good walk through of Node for beginners.
Installing the Grunt command line interface
To run our grunt
commands we need to install the Grunt tool globally, to do this with npm we need to run the following command on the command line.
npm install -g grunt-cli
To check it's installed correctly we'll check it's version:
$ grunt --version
grunt-cli v0.1.6
Great, now we're ready to set up our Grunt process.
Installing the Grunt npm packages we need
Node can do a lot of things and most of this functionality is delivered as packaged modules. A module is just a bundle of code, the "packaged" bit means that the code is bundled with a some meta-data that describes the module. The package can then be published to the npm module registry with enables it to be installed via npm. We've already installed one package above using npm install
but to build our optimised site we'll need to install a few more.
We could install all the tools we need one by one by running the following command in our project directory over and over again:
npm install grunt my-package-name
This would install the my-package-name package into a folder called node_modules
in our root directory. This is a bit laborious so there's an easier way.
Using the package.json file
To enable us to install packages easily we need to make our project a package (how recursive!) We need to add a file called package.json
to our project and then define our dependencies (e.g. the modules we're going to use in our project) and then npm can read this and install them automatically.
So, when we add package.json
to our project folder what do we put in it? Well, as the name suggests it's a JSON file that has a particular format, here's the one from this project:
{
"name": "basic-grunt-build",
"version": "0.1.0",
"dependencies": {
},
"devDependencies": {
"grunt": "~0.4.1",
"load-grunt-tasks": "~0.1.0",
"grunt-contrib-clean": "~0.5.0",
"grunt-usemin": "~0.1.11",
"grunt-concurrent": "~0.3.0",
"grunt-contrib-imagemin": "~0.2.0",
"grunt-svgmin": "~0.2.0",
"grunt-contrib-htmlmin": "~0.1.3",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-cssmin": "~0.6.0",
"grunt-contrib-uglify": "~0.2.0",
"grunt-rev": "~0.1.0"
},
"engines": {
"node": "0.10.x"
}
}
Let's go through each part one by one:
- name - the name of your package/site
- version - the version number of your package/site, use semantic versioning
- dependencies - the npm modules your need for production - as we're just using grunt to build our site, not serve it, we won't put any packages in here.
- devDependencies - the packages we'll need in development, you can see our Grunt dependencies here, don't worry too much about them for now, we'll go through them one by one below
- engines - the version of Node we'll use, currently we're on 0.10
WAT?: You may wonder why we have Grunt in our dependency list - didn't we just install that? Actually we just installed the command line interface, not the package that does all the work. npm installs all dependencies locally to the project - this may be a bit of change if your used to other dependency managers but it definitely has a lot of advantages.
Now we've got that set up we can go into the root of our project folder and just type
npm install
and npm will automatically install all our required modules into node_modules
TIP: If you want to install a new Grunt package and add it to the devDependencies list at the same time just add the --save-dev
switch to your install command:
npm install your-grunt-package --save-dev
Setting Up Our Gruntfile
Finally, we're ready to start writing our build process. To do this we're going to be writing a Gruntfile - this is a list of tasks that do the various things we want to do, and then we compose these tasks into commands. By the end of the section we'll be able to run grunt dist
and have an optimised build of our site ready to deploy.
If you want to take a peek at the whole Gruntfile before we start have a look at it here XXX
Initial Setup
Lets have a look at the first couple of lines of the file:
module.exports = function (grunt) {
// ...grunt setup...
}
The module.exports
line is part of Node, it's the object that's returned when this file is required
by Node. In this case we're just creating a wrapper function that will contain all our config. It's not essential to understand this at the moment but if you want to find out more have a look at the documentation for Node's Module system.
The next line is a useful shortcut - we could load all our Grunt modules individually by writing something like the following in our Grunt file:
grunt.loadNpmTasks('grunt-contrib-clean');
but we're going to use the load-grunt-tasks package to auto-load them for us. When we call:
require('load-grunt-tasks')(grunt);
we are loading the load-grunt-tasks
package and telling it to load all the npm packages it can find in the package.json file that begin with 'grunt'.
Now to the config proper, the next call is the to the initConfig
function to which we'll pass our configuration object:
grunt.initConfig({
//...actual config...
})
We're going to pass it a big object literal that contains all our config for the individual tasks.
Before we pass in our conig we'll remove any 'magic paths' from or config by creating a paths object with the some useful paths:
appPaths: {
app: 'site',
dist: 'build_output/dist'
},
app
points to the root of our source files, in this case our site folderdist
points to the output folder we want to write our optimised files to, we'll put it in a sub-folder ofbuild_output
as we may want to have different builds at some point e.g. a debug build
And that's our intial setup done.
Configuring our Build Steps
Next we're going to configure each of the build steps, but first:
A Short Interlude on Globbing
Grunt mainly works on groups of files, so we need some way to tell it which files to work on. We could just give it a load of direct paths:
'site/index.html'
This is really brittle though, we really want to say something like "all html files" and we can do that with a Glob pattern. Grunt uses the node-glob module to generate arrays of paths that match patterns we supply. If we wanted to match all the html files in site folder we could use the following pattern:
'site/*.html'
// returns [ 'index.html' ]
The acts a a wildcard and will match any zero or more characters. If we wanted to find all the JS files below site we can use the Globstar pattern (*) which matches all folders recursively:
'site/**/*.js'
// [ 'js/bootstrap.js', 'js/jquery-1.10.2.js' ]
Finally, if we want to match several file types we can use a brace expansion to match multiple extensions like so:
'site/**/*.{js,html}'
// [ 'site/index.html', 'site/js/bootstrap.js', 'site/js/jquery-1.10.2.js' ]
Clean out our build folder using grunt-contrib-clean
Before we start we want to make sure we don't have any artifacts left over from our previous build - we'll use the grunt-contrib-clean task to remove all our previous files.
To use it in our project, we'll first add a clean section to our Gruntfile and then add a key of dist
which has as it's value the path of the folder we want to clean.
clean: {
dist: '<%= appPaths.dist %>'
},
Adding Usemin comments to our HTML
Usemin is probably the most complicated of the modules we'll use, mainly because it does the most work. Using special comment blocks we can tell Usemin what files we want to optimise and it will keep track of our original and optimised files and then in the final build stage, replace the references to the original files with their modified versions. This list of files is also used be used by other tasks to work out what files to process - by default it will automatically configure the concat and uglify tasks.
We need to give Usemin some information about what files we want to optimise and concatenated - to do this we use special comment blocks in our HTML. The basic format of these blocks are:
<!-- build:<type> <path> -->
... HTML Markup, list of script / link tags.
<!-- endbuild -->
For a more concrete example lets look at our index.html page blocks:
<!-- build:css styles/main.css -->
<link href="css/bootstrap.css" rel="stylesheet">
<link href="css/main.css" rel="stylesheet">
<!-- endbuild -->
In this block we're saying this is a build
of the css
type and we want to the optimised file to have the path styles/main.css
. When the process is finished we should have both the bootstrap and and main CSS files optimised and concatenated into a scripts/main.css
file.
Our JavaScript block is very similar:
<!-- build:js scripts/main.js -->
<script src="js/jquery-1.10.2.js"></script>
<script src="js/bootstrap.js"></script>
<!-- endbuild -->
We have a build
type of javascript
this time, and we want both the jquery and bootstrap files to be optimised and concatenated into a scripts/main.js
file.
Now we've added the comments to our HTML let's configure our Grunt task.
Configure grunt-usemin
Usemin works in two stages, useminPrepare
and usemin
. useminPrepare
scans our html for blocks and configures the concat and uglify task using the block comment info. To configure useminPrepare we just need to tell it which files to scan - we'll configure it to scan all the files in our site root directory with the extension .html
.
useminPrepare: {
html: '<%= appPaths.app %>/**/*.html',
options: {
dest: '<%= appPaths.dist %>'
}
},
Note: the /**/*.html
bit we're passing in is a file expansion or glob - basically it's just a pattern match like a regexp. The /**/
bit means match/look in any folder below our main app directory.
Use grunt-concurrent to run independent tasks in parallel
Now we've done all our setup we can get to the actual optimisation. Grunt can be quite slow so we'll want to speed up our build in any way we can. The easiest way to do this is run some tasks at the same time. We can use grunt-concurrent to do this, we'll just pass through an array of the tasks we can run concurrently.
concurrent: {
dist: [
'imagemin',
'svgmin',
'htmlmin'
]
},
WARNING: It's important to make sure these really are tasks that can be run independently and concurrently and don't rely on any other task - for example we can't put our cssmin
task in here as we haven't concatenated the files yet.
Optimise images with grunt-contrib-imagemin
We're finally going to optimise something! grunt-contrib-imagemin will crunch our images down by running various optimisations. We just need to tell it where to look for the files. Here's our config:
imagemin: {
dist: {
files: [{
expand: true,
cwd: '<%= appPaths.app %>/images',
src: '**/*.{png,jpg,jpeg}',
dest: '<%= appPaths.dist %>/images'
}]
}
},
In our dist task we're telling imagemin to:
- expand enable path expansion / globbing (see previous note) for this task
- cwd set our working directory to this path
- src the path expansion to use to find input files
- dest the output directory
Optimise SVG files with grunt-svgmin
Next we'll optimise any SVG files with grunt-svgmin, this is pretty much the same setup as imagemin apart from the .svg extension:
svgmin: {
dist: {
files: [{
expand: true,
cwd: '<%= appPaths.app %>/images',
src: '{,*/}*.svg',
dest: '<%= appPaths.dist %>/images'
}]
}
},
Minify our HTML with grunt-contrib-htmlmin
Finally minify our HTML with grunt-contrib-htmlmin, again pretty much the same setting apart from the file extension:
htmlmin: {
dist: {
options: {
},
files: [{
expand: true,
cwd: '<%= appPaths.app %>',
src: ['*.html', 'views/*.html'],
dest: '<%= appPaths.dist %>'
}]
}
},
Concatenate our files with grunt-contrib-concat
We've finished our concurrent tasks and now we want to concatenate our js and css files together using grunt-contrib-concat, but where is the config?
concat: {},
Our concat task has already been configured for us by the prepareUsemin
task above - no configuration needed! We can always add any custom options here if we need to, otherwise in this step we'll combine the CSS and JS files in our usemin block comments into the output file we specified.
Optimise our CSS with grunt-contrib-cssmin
Now that the CSS is concatenated lets optimise it with grunt-contrib-cssmin, but again there's no config?
You probably guessed it's usemin again, it's automatically configured this task for us but we can always add custom settings in here if we want to.
cssmin: {},
Optimise our JS with grunt-contrib-uglify
As with the CSS, we can now optimise the concatenated JS with grunt-contrib-uglify - we'll use Uglify to do this. Again prepareUsemin has done most of our work for us, we'll just add one option:
uglify: {
options: {
preserveComments: 'some'
}
},
preserveComments: 'some'
just tells Uglify to preserve the license comments in our jQuery and Bootstrap JS files.
Add a revision has to our file names with grunt-rev
Now we've generated all our files let's add some revision hashes to the filenames with grunt-rev to ensure we can cache these files FOREVER! Our revision task will add a file content hash prefix to our file names and work with Usemin to ensure that the original file references are changed to the new rev'd ones.
Setup is easy, we just give it the file expansions for the files we want it to rev:
rev: {
dist: {
files: {
src: [
'<%= appPaths.dist %>/scripts/**/*.js',
'<%= appPaths.dist %>/styles/**/*.css',
'<%= appPaths.dist %>/images/**/*.{png,jpg,jpeg,gif,webp,svg}'
]
}
}
},
Update the file paths with usemin
Now all our optimisation is done Usemin needs to replace the original source filepaths with our newly generated optimised, concatenated and rev'd files.
usemin: {
html: ['<%= appPaths.dist %>/{,*/}*.html'],
css: ['<%= appPaths.dist %>/styles/{,*/}*.css'],
options: {
dirs: ['<%= appPaths.dist %>']
}
},
Here we're just giving it file expansions of what files to scan for replacements.
Copy any other files with grunt-contrib-copy
Are there any other files that we need to copy over that aren't handled by our optimise steps? Fonts! Lets copy them across with the grunt-contrib-copy task:
copy: {
dist: {
expand: true,
cwd: '<%= appPaths.app %>',
dest: '<%= appPaths.dist %>',
src: [
'fonts/**/*',
]
}
},
This should be pretty self explanatory - we're just setting expand to true, setting the working directory and destination directory then adding some source paths (in this case our fonts) and saying copy all files from there to the dist directory.
Configuring our Task
Lastly we'll configure the composite task that we'll call from the command line, this is just a call to registerTask
with a name and an array of steps to run
grunt.registerTask('dist', [
'clean:dist',
'useminPrepare',
'concurrent:dist',
'concat',
'copy:dist',
'cssmin',
'uglify',
'rev',
'usemin'
]);
Now we have a command we can run by entering grunt dist
in our root director and it'll step through all our tasks.
The Moment of Truth
Navigate to your root directory, run grunt dist
and we'll have a lovely optimised site in build_output/dist
How much faster is it? Let's see:
4 requests, DOMContentLoaded at 619 milliseconds - nice work, nearly 40% speed up - Site Optimisation Achievement Unlocked!
Epilogue
Hopefully you've got a good basic understanding of Grunt now and see how useful a tool it is. There's a whole lot more you can do with it including integrating it into other frameworks - have a look at the Plugin list and see what else it can help you with.
I'm on Twitter, @dburrows innit.