Minifying + Compressing an AngularJS App

This short tutorial demonstrates how to prepare an AngularJS app for deployment to a static web server, with all the bells and whistles needed to score an A on YSlow.

Key Assumptions

  1. Your dev setup is on Linux, or other Bash-compatible. (Build script written for Bash.)
  2. You use Git for SCM. (The build script will use git for some operations. Feel free to alter the script, and get rid of this dependency.)
  3. The app is structured as recommended by the angular-seed project. (Build script expects certain folders to be present at specific locations. You can adapt it to your project structure.)

Dependencies

Go grab the following:

  1. YUI Compressor
  2. Git
  3. Gzip (Installed by default on most *NIX systems. Pull from distro repos otherwise.)
  4. Node.js (For testing stuff on your localhost. Not required if you have/prefer some other server for delivering static content.)
  5. Stomach for shell scripts

Prologue

I wanted to split my web application into two distinct components:

  1. A client-side, JS-driven presentation layer.
  2. A lightweight, REST-based backend.

I’ve had to sort out a lot of issues to get both to cooperate while running on different servers on different domains, use digest-based authentication instead of cookies (REST is stateless), and so on, but that’s another post. This one focuses on efficiently delivering the UI portion – HTML + CSS + JS + Media – which from a server POV is static content.

Preparing AngularJS Scripts for Minification

The AngularJS docs provide some information on how to prepare controllers for minification here. Quoting from the page:

Since angular infers the controller’s dependencies from the names of arguments to the controller’s constructor function, if you were to minify the JavaScript code for PhoneListCtrl controller, all of its function arguments would be minified as well, and the dependency injector would not be able to identify services correctly.

PhoneListCtrl is part of the angular-phonecat application, used for driving the on-site tutorial.

Basically, every controller defined by your application needs to be explicitly injected with whatever dependencies it has. For the example above, it looks something like:

PhoneListCtrl.$inject = ['$scope', '$http'];

There is one more way defined on the site, but I prefer the method above.

However, this is not enough to get minified scripts working right. YUI Compressor changes closure parameter names, and this doesn’t go down well with Angular. You need to use inline annotations in defining custom services. You can find a usage example here.

Additionally, you can collate all content from controllers.js, directives.js, services.js and filters.js into app.js to reduce the number of calls made to the server.
Don’t forget to modify your index.html / index-async.html to reflect this change.

The Build Script

If you’re sticking to the folder structure provided by angular-seed, you’ll have an app folder in your project root. Adjacent to this, create a build folder to contain the minified and compressed output files generated by the build script. You can tell git to ignore this folder by adding the following line to .gitignore:

/build/*

You can put your build script anywhere you like, and run it from anywhere in the project folder. I have put it inside the conveniently provided scripts folder.

#!/bin/bash

ccred=$(echo -e "\033[0;31m")
ccyellow=$(echo -e "\033[0;33m")
ccgreen=$(echo -e "\033[0;32m")
ccend=$(echo -e "\033[0m")

exit_code=0

cd "$(git rev-parse --show-toplevel)"

#Minify
echo -e "$ccyellow========Minify========$ccend"
for ext in 'css' 'js'
do
    for infile in `find ./app -name *.$ext |grep -v min`
    do
        outfile="$(echo $infile |sed 's/\(.*\)\..*/\1/').min.$ext"
        echo -n -e "\nMinifying $infile to $outfile: "
        if [ ! -f "$outfile" ] || [ "$infile" -nt "$outfile" ]
        then
            yuicompressor "$infile" > "$outfile"
            if [ `echo $?` != 0 ]
            then
                exit_code=1
                echo -e "\n$ccred========Failed minification of $infile to $outfile . Reverting========$ccend\n" >&2
                git checkout -- "$outfile" || rm -f "$outfile"
            else
                echo $ccgreen Success.$ccend
            fi
        else
            echo $ccgreen Not modified.$ccend
        fi
    done
done

#Compress / Copy
echo -e "\n\n$ccyellow========Compress / Copy========$ccend\n"
for infile in `find ./app -type f -not -empty`
do
    filetype="$(grep -r -m 1 "^" "$infile" |grep '^Binary file')"
    outfile="./build/$(echo $infile |cut -c7-)"

    mkdir -p $(dirname "$outfile")
    if [ ! -f "$outfile" ] || [ "$infile" -nt "$outfile" ]
    then
        if [ "$filetype" = "" ] #Compress text files
        then
            echo -n -e "\nCompressing $infile to $outfile: "
            gzip -c "$infile" > "$outfile"
        else #Copy binary files as is
            echo -e -n "\nCopying $infile to $outfile: "
            cp "$infile" "$outfile"
        fi
        if [ `echo $?` != 0 ]
        then
            exit_code=2
            echo -e "\n$ccred========Failed compress / copy of $infile to $outfile . Reverting========$ccend\n" >&2
        else
            echo $ccgreen Success.$ccend
        fi
    else
        echo -e "$infile -> $outfile: $ccgreen Not modified.$ccend\n"
    fi
done

echo -e "\n$ccyellow========Finished========$ccend"
exit $exit_code

Once you run this script, every app/file.[css | js] would have a working copy at build/file.min.[css | js]. Every other file in the app folder will be either:

  1. compressed and copied (name unchanged) into the build folder if it is a text file, or
  2. simply copied into the build folder if it is a binary file (like an image).

Your CSS and JS references need to be updated to their corresponding min versions in index.html / index-async.html.

Now that you’ve got a compressed, minified version of your app in the build folder, you can deploy it to any static server. But you do need to set your HTTP response headers properly, or the browser WILL show garbage. Most importantly, any compressed content must be served with the HTTP response header:

Content-Encoding: gzip

Additionally, for every file that is static content, it makes sense to set a far future date using an Expires header similar to the following:

Expires: Thu, 31 Dec 2037 20:00:00 GMT

The NodeJS web-server.js Script

The contents of the build folder are technically ready to be uploaded to any web server, but you will probably want to run the app from your localhost to first check if everything works fine. The built-in web-server.js is very useful to quickly launch and test your app, but it needs a few mods in order to serve the compressed content from the build folder correctly. The Content-Encoding header is sufficient to render the page correctly, but if you’re a stickler for good YSlow grades even on your localhost, you will want to add the Expires headers as well. Search for the following response codes in your web-server.js and add the lines listed below:

  1. 200 (writeDirectoryIndex), 500, 404, 403, 301:
    'Expires': 'Thu, 31 Dec 2037 20:00:00 GMT',
    
  2. 200 (sendFile) (After var file = fs.createReadStream(path);):
    var fileType = StaticServlet.MimeMap[path.split('.').pop()];
    var contentType = fileType || 'text/plain';
    res.writeHead(200, {
        'Content-Type': contentType,
        'Expires': 'Thu, 31 Dec 2037 20:00:00 GMT',
        'Content-Encoding': ((path.indexOf('./build') === 0) && ((contentType.indexOf('text') === 0) || (contentType.indexOf('application/javascript') === 0))) ? 'gzip' : ''
      });
    

That’s it! Now when you run the web-server.js, all content from the build folder will be correctly served with the ‘gzip’ header (unless it is a binary).

, , , , ,

  1. #1 by Ken on January 24, 2014 - 9:36 am

    For me, I don’t understand why so many people don’t deal with this minification/concatenation of their assets on the actual web server. You want to be able to switch between development/production assets easily, and you shouldn’t have to constantly change your index files to use one or the other. This is such a terrible practice, yet the web is filled with tons of examples like this, from angular to every other framework.

    What you want to do is always point to app.css or app.js, and depending on the mode your web app is running in, it will be either compressed or not compressed, and cached or not cached. This way you no longer have to do any busy work when getting your website ready for production – you just flip a switch and enjoy your 98% performance rating using google’s page speed.

    Actually asking developers to manually run tools to do this is just asking for trouble, especially for bigger projects.

    • #2 by Aditya Mukhopadhyay on January 24, 2014 - 10:12 am

      You’re right about eliminating manual steps – a grunt task will automatically take care of all the minification/compression requirements at build time, though performing these steps on the fly on the server may not always be an accessible option.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 386 other followers

%d bloggers like this: