Using GruntJS to Bundle and Minify JavaScript and CSS
Bundling and minifying static assets like JavaScript and CSS files has been a staple to improve page load time. The goal is to reduce the number of HTTPS request and the size of the files.
HTTP requests are expensive to create, which delays your page from loading. Bundling concatinates files into a single file. CSS and JavaScript files can be combined into a single file, reducing the number of reuqests and eliminating additional delays while the browser evaluates the code.
GruntJS is a web task runner built on top of NodeJS. The grunt ecosystem has thousands of useful plugins that should automate just about every task you might have. Grunt has two plugins to bundle and minify CSS and JavaScript.
In Visual Studio you are probably used to pressing Ctrl + Shift + B to compile a solution and creating a build script for continuous integration tools. That kicks off the process to build your assemblies and executables. But what about that JavaScript and CSS? ASP.NET includes a bundling and minification solution, but it requires compiled C# that can become a bit hectic to maintain. Visual Studio also offers some static analysis of JavaScript and CSS, especially when Web Essentials is installed. Other editors like Sublime also offer plugins to help with the web build process. Grunt shines as an independent, cross-platform command line solution.
Bundling and minification is one of the most overlooked steps for modern web applications. Despite the overwhelming research that shows bundling and minification dramatically improves a web page's load time, very few web sites implement it. According to HTTPArchive the average web page makes 17 requests for JavaScript files and 5 external CSS files. When I randomly survey sites I rarely encounter sites with more than 50 JavaScript files and most of them are not minimized. Tools like Grunt make this step very easy to implement, eliminating everyone's excuse.
Grunt requires NodeJS, which can easily be installed by navigating to the NodeJS home page. You should see a large 'Install' button. Tapping it will install node on your system, don't worry it is smart enough to know what platform you are using. Once node is setup you can install the Grunt CLI or command line interface from the Node Package Manager (npm). The package manager is similar to NuGet for Visual Studio users. NPM is node command line installer. Grunt can be installed globally by entering npm -g install grunt-cli on the command line.
The next step is to configure your project. This is done by creating a package.json file. Node uses package.json files to know what modules should be installed. Once configured you can either run npm install or npm update to update currently installed modules. This article assumes Grunt is the only node tool you will be using, so the dependencies are all Grunt and Grunt plugins.
In my latest book the movie application uses Grunt as its web build tool. This is the package.json file:
{
"name": "Modern-Web-Movies",
"version": "0.1.0",
"author" : "Chris Love",
"private" : true,
"devDependencies": {
"grunt" : "~0.4.*",
"grunt-contrib-cssmin": "*",
"grunt-contrib-qunit": "*",
"grunt-contrib-jshint": "*",
"grunt-contrib-uglify": "*",
"matchdep": "*"
}
}
The file contains, as its extension implies, a JSON (JavaScript Object Notation) object. The first four properties define the project's name, version, author and if it is a private application. The devDependencies member defines the node modules needed to execute the application. The movie application uses some of the Grunt contrib modules, cssmin, qunit, jshint and uglify. The matchdep module keeps you from having to manually load each grunt plugin.
Grunt is actually configured using the gruntfile.js, which contains another JSON object. This one drives grunt with configuration for each module and order in which each task and configuration should execute. The following is a version of the movie application's gruntfile with just the bundling and minification tasks configured:
module.exports = function (grunt) {
require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks);
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
cssmin: {
sitecss: {
options: {
banner: ''
},
files: {
'css/site.min.css': [
'css/debug/site.css',
'css/debug/animations.css',
'css/debug/toolbar.css',
'css/debug/touch.css',
'css/debug/panorama.css',
'css/debug/movie.app.home-view.css',
'css/debug/movie.app.forms.css',
'css/debug/movie-grid.css',
'css/debug/movie.app.maps-view.css',
'css/debug/movie.app.movie-view.css',
'css/debug/movie.app.movies-view.css',
'css/debug/movie.app.nav.css',
'css/debug/movie.app.search-view.css',
'css/debug/movie.app.theater-view.css']
}
}
},
uglify: {
options: {
compress: true
},
applib: {
src: [
'js/libs/dollarbill.min.js',
'js/libs/reqwest.js',
'js/libs/rottentomatoes.js',
'js/libs/fakeTheaters.js',
'js/libs/movie-data.js',
'js/libs/backpack.js',
'js/libs/deeptissue.js',
'js/libs/toolbar.js',
'js/libs/mustache.js',
'js/libs/panorama.js',
'js/libs/spa.js',
'js/libs/rqData.js',
'js/debug/movie.app.js',
'js/debug/movie.app.grid.js',
'js/debug/movie.app.home-view.js',
'js/debug/movie.app.account-view.js',
'js/debug/movie.app.maps-view.js',
'js/debug/movie.app.movie-view.js',
'js/debug/movie.app.movies-view.js',
'js/debug/movie.app.news-view.js',
'js/debug/movie.app.search-view.js',
'js/debug/movie.app.privacy-view.js',
'js/debug/movie.app.search-view.js',
'js/debug/movie.app.theater-view.js',
'js/debug/movie.app.notfound-view.js',
'js/debug/movie.app.bootstrap.js'
],
dest: 'js/applib.js'
}
}
});
// Default task.
grunt.registerTask('default', ['uglify', 'cssmin']);
};
Let's break this file down into smaller parts. First the module.exports is the entry point for Grunt to execute under node.
module.exports = function (grunt) {
//configuration here
};
Next the matchdep node module is used to load each Grunt task. Without the matchdep module each task would need to be registered by hand. For a small configuration like this it is not a big deal. More complex Grunt configurations can be much more verbose.
require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks);
Without matchdep each module must be registered like this:
// Load the plugin that provides the "uglify" task.
grunt.loadNpmTasks('grunt-contrib-uglify');
The next section defines the actual Grunt configuration. First you point Grunt to the package.json file. It will use this configuration to ensure all the plugins are up to date. Next you configure each module, here just the cssmin and uglify plugins are defined. CSSmin bundles and minifies CSS files, Uglify does the same for JavaScript.
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
cssmin: {}
},
uglify: {}});
Each module has its own configuration options, you need to check each plugin for specifics. Most plugins need source files and a destination or output file. Here the CSSmin plugin defines a banner that is prepended to the minimized CSS. This is followed by the files. There are two common ways to define source and destination files. The cssmin configuration uses the files object which has a single property, the destination file path. The destination path is a property with a value of an array containing the source files. The source file array is arrange in the order the files should be bundled. The output file will contain the product of these files, minimized for production.
cssmin: {
sitecss: {
options: {
banner: '/* My minified css file */'
},
files: {
'css/site.min.css': [
// source files
]
}
}
},
Similarly the uglify module needs to be configured. Here the uglify compress option is set to true, for more details on uglify module read the documentation. The source and destination file configuration is done slightly differently than the cssmin configuration. Here it defines a sub task name, 'applib' with the value of a JavaScript object defining the source files and the destination file. Again the source file configuration is the order the files need to be bundled together. Remember to place your highest dependency first. The destination file is the bundled and minified version of the JavaScript files.
uglify: {
options: {
compress: true
},
applib: {
src: [
// source files
],
dest: 'js/applib.js'
}
}
The last step is running the grunt-cli. For Windows this is simple. Open a command prompt and change the directory to the project's working directory. Once you have done this you only need to run the grunt.cmd command. This will load the gruntfile and execute the configured tasks.
That is all you need to have an automate web build system. This just shows how to bundle and minify, but there are so many more things Grunt can automate for you. This configuration takes about 5-10 minutes to setup. The movie application uses the bundling and minification tasks to reduce 27 JavaScript files to a single file containing 77kb, about half of what the original files contained. The CSS is reduced from 14 files to a combined file of <36kb, saving around 12kb total. Chapter 19 in my book, High Performance Single Page Web Applications shows how to use Grunt for more than just bundling and minifying.