Minify and Compress all your JavaScript files into One, on the Fly

As my applications have grown in complexity, I’ve followed a path probably quite similar to many of you with respect to .js file maintenance. In the beginning I had one js file to include in the site’s/app’s header, containing just a few basic js functions used across the site.

As my JavaScript codebase grew, I added more .js files, trying to specialize the files and even went through the trouble of including specific files on some pages and not on others. Then I adopted a framework (jQuery in my case) and that is just one more script tag.

At some point I became aware of the minifcation trend for JS and CSS files, and began looking at how much bandwidth I could save per page load by doing so.  Using an online minifier, I began minifying each .js file after every modification.  This became very unmanageable very quickly. I also had to consider the impact of multiple file loads on the browser and how that impacts performance.

I decided to find a way to automate this process.  I stumbled upon the JSmin class PHP class, which is an implementation of Douglas Crockford’s JSMin.  The solution would implement JSmin, but with a wrapper class that would read each .js file, minify (and compress if possible), and then output into a single file.  More helpful ideas were found in this blog article at verens.com.

What I came up with accomplishes the following:

– Given an array of .js filenames, reads and minifies each, writes to a single new file.

– Reads file modification date of each file, if none are newer than the auto-generated output file, the process is skipped.

This results in an on-the-fly minifier that only runs when JavaScript code has been modified in any one of the original files.  This makes code deployment simpler….just sync updated js files to the appropriate directory.

I’ve encountered a couple of negatives which are easily mitigated.  First, in production the process is slow…sometimes 15 seconds. That first user to hit the site after a new js file has been uploaded is going to think the server is down.  Remedy this by uploading at off-peak times and immediately surf to the site yourself, saving an unwitting user the 15 second wait.  Second, I’ve experienced some kind of funky file collision on occasion which resulted in the minification running on every page load (think 15 second page loads for every page, every time), so when syncing from test to prod I will typically delete the generated file from test first, so prod can then generate its own clean file.

So here’s the script:

/**
 * Wrapper class for JSMin javascript minification script
 *
 * based on http://verens.com/archives/2008/05/20/efficient-js-minification-using-php/
 *
 * @author Chris Renner
 */
 
include('JSMin.php');
 
class App_Minifier {
 
    /**
     * Constructor not implemented
     */
    public function __construct() {}
 
    /**
     * Concatenate and minify multiple js files and return filename/path to the merged file
     * @param string $source_dir
     * @param string $cache_dir
     * @param array $scripts
     * @return string
     */
    public static function fetch($source_dir, $cache_dir, $scripts) {
 
        $cache_file = self::get_filename($scripts);
 
        $result = self::compare_files($source_dir, $cache_dir, $scripts, $cache_file);
 
        if(!$result) {
 
            $contents = NULL;
 
            foreach($scripts as $file) {
 
                $contents .= file_get_contents($source_dir . '/' . $file . '.js');
 
            }
 
            // turned off due to performance issues on production 6-9-10
            $code = "";
 
            $minified  = JSMin::minify($contents);
 
            $fp = @fopen($cache_dir . '/' . $cache_file, "w");
            @fwrite($fp, $minified);
            @fclose($fp);
 
	}
 
        return $cache_dir . '/' . $cache_file;
 
    }
 
    /**
     * input array of js file names
     * converts array into string and returns hash of the string
     * as the new filename for the minified js file
     * @param array $scripts
     * @return string
     */
    public static function get_filename($scripts) {
 
        $filename = md5(implode('_', $scripts)) . '.js';
 
        return $filename;
 
    }
 
    /**
     * we're going to compare the modified date of the source files
     * against the hash file if it exists and return true if the hash
     * file is newer and
     * return false if its older or if hash file doesn't exist
     * @param string $source_dir
     * @param string $cache_dir
     * @param array $scripts
     * @param string $cache_file
     * @return boolean
     */
    public static function compare_files($source_dir, $cache_dir, $scripts, $cache_file) {
 
        if(!file_exists($cache_dir . '/' . $cache_file)) {
            return false;
        }
 
        $cache_modified = filemtime($cache_dir . '/' . $cache_file);
 
        foreach($scripts as $source_file) {
 
            $source_modified = filemtime($source_dir . '/' . $source_file . '.js');
 
            if($source_modified > $cache_modified) {
                return false;
            }
 
        }
 
        return true;
 
    }
 
}

And here’s how you would call it in your boostrapping file, etc.

// create array of .js filenames to be minified
$scripts = array('jquery', 'jquery.colorbox', 'jquery.livequery', 'jquery.tipsy', 'jquery.validate', 'functions', 'menu', 'childtables', 'datepicker');
 
// call the fetch static method, supplying the source dir, target dir and the scripts array
$scriptfile = App_Minifier::fetch('scripts', 'temp', $scripts);
 
// put the result in a script tag in your html header

Yes I realize that a static class perhaps wasn’t the best choice, but it works and it keeps memory usage to a minimum. I’d probably write it differently today, and may yet refactor it to remove the static.

The output $scriptfile will be a .js filename, generated by hashing the concatenation of all the filenames in the scripts array. This permits different combinations of files to produce different output files, if that’s something you need.

Also note my comment in fetch() about the gzip feature not being used. This caused problems in my particular environment so I’m not using it, but it may work for some of you and I’d be eager to hear from you if it does. To enable, just change line 50 from

$minified  = JSMin::minify($contents);

to

$minified  = JSMin::minify($code . $contents);

In my specific example I was loading as many as 9 different .js files per page load, totaling 250kb. Now all that JavaScript loads in 1 file measuring 147kb.

Oh, and don’t forget to download JSMin.php from github.