(4 minute read)
Recently I've been focussing more and more on performance in the web projects I'm working on. By now the benefits of merging, minifying and GZipping external Javascript and CSS files are well documented - your web page loads quicker (due to smaller download sizes and less HTTP requests to make). For a web project I'm currently working on I decided to investigate what tools were available out there and particularly, how I could automate the whole process using a build script. My requirements are quite simple:
There are a few established Javascript minifiers out there - the main two I came across being Google Closure and YUI Compressor. YUI Compressor minifies CSS as well as Javascript files (and I planned to do both). There are comparisons out there between the various toolkits but I already prefer YUI Compressor since it does CSS files too. However, neither of these tools have any concept of inter-file dependencies as their main focus is to simplify minify a given set of input files.
Some further searching on the web brings up RequireJS and Juicer. RequireJS provides a small Javascript library which enables you to split your Javascript code into modules where each module lies within its own script file and can assert dependencies on other modules. The RequireJS code will ensure that modules are loaded in the right order when needed. Moreoever, it comes with an optimisation tool (internally utilising Closure) which you can use to merge and minify modules into single files when it comes time to deploy your application. The optimisation tool also minifies CSS files, though it doesn't process @import statements. Juicer, on the other hand, is simply a minification tool (internally utilising Closure or YUI Compressor) which can process CSS @import statements. It also provides a custom @depends declaration for use in Javascript files to determine inter-file dependencies though this is optional.
Taking things into consideration I've personally opted for Juicer over RequireJS. For my project I don't need to merge Javascript files just yet since everything is already nicely modularized, but I do need the kind of CSS minification which Juicer provides. In addition, using RequireJS would require me to rewrite my Javscript code whereas the Juicer method wouldn't, though the benefits of modularized Javascript outweigh the cons in the long run. So I might switch the Javascript minification to RequireJS later on once my scripts get more complicated.
Getting Juicer installed is easy:
... // assuming you have Java already installed
...
$ sudo apt-get install rubygems ruby ruby-dev libxslt-dev libxml2-dev
...
$ sudo gem install juicer
...
$ sudo ln -s /var/lib/gems/1.8/gems/juicer-1.0.8/bin/juicer /usr/local/bin/juicer
...
$ juicer install yui_compressor
$ juicer install jslint
You may have noticed the installation of JSLint above. This is used by Juicer the verify the Javascript code integrity prior to the minification process - but we can tell Juicer to ignore the result of this and proceed with minification anyway. You can run Juicer from the command-line right now:
$ juicer merge myfile.css
... // will produce myfile.min.css containing merged and minifed CSS
$ juicer help
... // will show help on commands and options
By default Juicer stores the minifed output in the same folder as the original file. This is ok when you have only one or two files but not when you have many. I want minified files to be stored in a folder parallel to the original folder. For instance, all my CSS files are stored under css/ and some in sub-folders of that folder. Thus, the minifed version of css/forms/base.css should be output at css_min/forms/base.css. All other CSS files under css/ should be minifed and output to the same relative path under css_min.
The benefit of storing the minifed version under a different folder is that it makes it easy to delete existing minified resources if need be. Additionally, when it comes to minifying Javascript some of the script files I have are third-party files which are already minified - storing the minifed version of a script file in a separate folder means not having to work out whether a minified file in the original folder is one that was already minified or one that was minified from an original non-minified source.
Note: You will need to modify your application code such that it is able easily switch the root folder it serves CSS and Javascript resources from (e.g. css or css_min). In my application I have it serve from the original folders when in development mode and from the minification output folders when in production deployment.
The folder for storing minifed CSS files should always be sibling to the original CSS folder in order for relative CSS image paths to remain accurate. For instance, if css/forms/base.css contains the following:
button {
background: transparent url("../../img/bg.png");
}
The minifed version of the CSS file should also be stored in such a way that the relative image path "../../img/bg.png" still makes sense. Taking all of the above into consideration, if the following is the folder structure containing the original source files:
project/css/base.css
project/css/forms/base.css
project/js/base.js
project/js/net/sockets.js
project/img
Then the minifed versions would be stored as follows:
project/css/base.css
project/css/forms/base.css
project/css_min/base.css
project/css_min/forms/base.css
project/js/base.js
project/js/net/sockets.js
project/js_min/base.js
project/js_min/net/sockets.js
project/img
The next step is to automate the minification process using a Gradle build script. In my Gradle script (below) I'm also GZipping the minifed CSS files:
// Create given folder if it doesn't exist
void ensure_folder_exists(String folderPath) {
File f = new File(folderPath)
if (!f.exists()) {
println "<< Creating folder: $folderPath >>"
f.mkdir()
}
}
task minify_css << {
println "\nMinifying CSS..."
String outputFolder = "css_min"
ensure_folder_exists(outputFolder)
FileTree files = fileTree(dir: "css", include: "**/*.css")
files.each { File file ->
String fileName = file.getName()
String fileFolder = file.getParent()
String fileOutputFolder = outputFolder fileFolder.substring(fileFolder.indexOf("css") 3)
ensure_folder_exists(fileOutputFolder)
String outputFilePath = fileOutputFolder "/" fileName
println "-- $fileName => $outputFilePath"
ant.exec(executable: "/usr/local/bin/juicer") {
arg(value: "merge")
arg(value: "-o" outputFilePath)
arg(value: "-f") // force overwrite target file
arg(value: file.toString())
}
}
}
task zip_css(dependsOn: minify_css) << {
println "\nZipping CSS..."
FileTree files = fileTree(dir: "css_min", include: "**/*.css")
files.each { File file ->
String fileName = file.getName()
println "-- $fileName => ${fileName}.gz"
ant.gzip(src: file, destfile:file.toString() ".gz")
}
}
And here's the equivalent for Javascript files. Note that when minifying Javascript files I first check to ensure that the file isn't already minified. I also tell Juicer to ignore JSLint verification results since it throws up lots of false negatives for my code:
task minify_js << {
println "\nMinifying JS..."
String outputFolder = "js_min"
ensure_folder_exists(outputFolder)
FileTree files = fileTree(dir: "js", include: "**/*.js")
files.each { File file ->
String fileName = file.getName()
String fileFolder = file.getParent()
String fileOutputFolder = outputFolder fileFolder.substring(fileFolder.indexOf("js") 2)
ensure_folder_exists(fileOutputFolder)
String outputFilePath = fileOutputFolder "/" fileName
// don't minify already minified files
if (-1 == fileName.indexOf(".min.")) {
println "-- $fileName => $outputFilePath"
ant.exec(executable: "/usr/local/bin/juicer") {
arg(value: "merge")
arg(value: "-o" outputFilePath)
arg(value: "-i") // skip JSLint errors
arg(value: "-f") // force overwrite target file
arg(value: file.toString())
}
} else {
println "-- $fileName => $outputFilePath (SKIP/COPY)"
ant.copyfile(src: file, dest: outputFilePath)
}
}
}
task zip_js(dependsOn: minify_js) << {
println "\nZipping JS..."
FileTree tree = fileTree(dir: "js_min", include: "**/*.js")
tree.each {File file ->
String fileName = file.getName()
println "$fileName => ${fileName}.gz"
ant.gzip(src: file, destfile:file.toString() ".gz")
}
}
I've integrated with the above Gradle script with my automated build process so that minifed, gzipped resources get created as part of my continuous integration process.
If you're using nginx or Apache to serve up the CSS and Javascript files then you can configure them to serve up GZipped versions of these resources if available. I'm using nginx and it has a module which does this. Here are the settings I use:
server {
...
# Serve static files directly
#
# /var/www/static/css/...
# /var/www/static/css_min/...
# /var/www/static/js/...
# /var/www/static/js_min/...
#
location ^~ /static/ {
root /var/www;
}
#
# serve pre-compressed static files
#
gzip off;
gzip_static on;
gzip_http_version 1.1;
gzip_proxied any; # Enables compression for all proxy requests
gzip_vary on; # Enables response header of "Vary: Accept-Encoding"
...
}
Let me know if you have any questions about the above or if you've come across better tools for the job.