Automate Javascript and CSS minification with Python

Sep 30, 2012

An important aspect of web usability is how long it takes pages to load and display since this is time that your users are of waiting on your site instead of using it. For this reason, it’s worthwhile to use one of the methods available to minimize your Javascript and CSS files. By ‘minimize’ I mean strip out unnecessary stuff like whitespace. Some minifiers go farther and rename variables to make them shorter as well as other tricks.

There are now numerous Javascript and CSS minifiers available, such as Closure, YUI Compressor, and of course the seminal JSMin, but I wanted something easy to integrate with Python because I like to use Django to build websites. For this I chose SlimIt.

For CSS minification which SlimIt does not support, I chose rCSSMin. There is also a related Javascript utility, rJSmin, which you could use instead of SlimIt. I chose SlimIt because it is optimized more for output size while rJSmin is focused mainly on execution speed of the minification itself.

So now we are going to talk about how to do this yourself.

Install SlimIt

SlimIt is available from PyPi at http://pypi.python.org/pypi/slimit

You will probably need to use ‘sudo’ to run many of these commands with appropriate permissions unless you like to hang around logged in as root which is not usually a great idea.

You can use pip to easily install SlimIt from PyPi.

If you don’t have pip, and you are on CentOS or RHEL or Fedora, you can install pip first with this command.

[sudo] yum install python-pip

If or when you have pip ready you can install SlimIt using this command.

[sudo] pip install slimit

On my CentOS systems, pip is named ‘pip-python’ instead.

Install rCSSmin

To install rCSSmin, download the bzipped tar file (or gzipped or the .zip) from it’s page and decompress the archive, then run ‘python setup.py install’ in the directory created. Assuming you have wget installed, you can do this like so:

wget http://storage.perlig.de/rcssmin/rcssmin-1.0.1.tar.bz2
tar -xjf rcssmin-1.0.1.tar.bz2
cd rcssmin-1.0.1
[sudo] python setup.py install

Automating minification

Now that you have SlimIt and rCSSmin both installed, it’s time to write a little code of our own to concatenate our Javascript source files together and then call these utilities to minify that. This way you can load the alternate non-minified versions when you are debugging, and you can also organize your Javascript and CSS source files as convenient to make the code manageable but still only make the web site viewer wait on a couple of connections and round trips to the server to download them.

Below is a Python program to concatenate Javascript source files and output one version for debugging and another minified version for production use.

The rCSSMin module supports preserving CSS comments which begin with ! character (bang) so that licenses and other important comments can be preserved.

Unfortunately, SlimIt and also rJSmin both seem to lack an equivalent capability, so I have setup the code to prepend license text to the minified version of the file.

In order to use this script, just replace the values in the jsSources and cssCources arrays with the paths to your source files, and change the values of jsDestPath, jsMinPath, cssDestPath, and cssMinPath to the paths where you would like to output your unminified and minified output files.

You should also replace jslicenses.txt with a file containing any copyright notices you need to maintain in the minified Javascript code.

Then you just need to run this script whenever you are ready to deploy your Javascript and CSS files to your server.

#!/usr/bin/python
from slimit import minify
from rcssmin import cssmin

def minifyCSSProc(srcText):
    return cssmin(srcText, keep_bang_comments=True)

def minifyJSProc(srcText):
    return minify(srcText, mangle=True, mangle_toplevel=True)

def doProcessFiles(minifyProc, sourcePaths, header, destPath, minPath):
    print "Combining to %s and %s" % (destPath,minPath)
    f = open(destPath, 'w')
    mf = None
    try:
        mf = open(minPath, 'w')
        mf.write(header)
        for srcFile in sourcePaths:
            print(srcFile)
            with open(srcFile) as inputFile:
                srcText = inputFile.read()
                minText = minifyProc(srcText)
            f.write(srcText)
            mf.write(minText)
    finally:
        f.close()
        if mf and not mf.closed:
            mf.close()

def doJSMin(sourcePaths, header, destPath, minPath):
    return doProcessFiles(minifyJSProc, sourcePaths, header, destPath, minPath)

def doCSSMin(sourcePaths, destPath, minPath):
    return doProcessFiles(minifyCSSProc, sourcePaths, '', destPath, minPath)

jsDestPath = "static/js/allmy.js"
jsMinPath = "static/js/allmy.min.js"
jsHeaderPath = "jslicenses.js"

jsSources = [ 
    "static/js/first-js.js",
    "static/js/second-one.js"
]

cssDestPath = "static/css/allmy.css"
cssMinPath = "static/css/allmy.min.css"

cssSources = [
    "static/css/top.css",
    "static/css/main.css"
]


if __name__ == '__main__':
    jsHeader = ''
    with open(jsHeaderPath) as f:
        jsHeader = f.read()
    doJSMin(jsSources, jsHeader, jsDestPath, jsMinPath)
    doCSSMin(cssSources, cssDestPath, cssMinPath)

Closing

This script or something like it can form part of your web deployment process which I recommend automating to prevent mistakes. You can combine this logic with other preprocessing steps and then automate something like rsync to upload your files. Just be careful the first few times when your deployment scripts are new!

References