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

by Chris Renner on 4/02/2011

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.

There are 14 comments in this article:

  1. 4/02/2011Kae Verens says:

    I just thought of another problem, which is easy to fix (must do so on my own CMS!)

    If it takes 15 seconds to run the minification, and 10 people request a fresh js script within a 15 seconds, range then that will spawn 10 separate minification processes.

    The solution is that when you’re starting the minification, you create a lock file (whatever.js.lock), and remove it when you’re finished.
    If a visitor requests a file and the lock is in place, then loop a “sleep” (system call) until the lock is gone, and send the fresh minified version.

  2. 11/07/2011EdC says:

    Another solution requires CRON capabilities. Run the PHP (or a command line equivalent) in the background every 15-30 minutes. If no changes, you wasted a few CPU cycles, but nothing versus making any user wait. You could also have a command line utility ‘watch’ for updates to a set of directories instead of CRON. Then just put an entry in your /etc/init.d/ to start up the watch utility on your JS directories.

  3. 23/07/2011Gabriele says:

    After concatenated $scriptfile was generated, how can I put the single pieces of javascript in the html pages? Thnk you.

  4. 24/07/2011Chris Renner says:

    the fetch() method returns a dir/filename combination. YOu just need to pass that in a variable into your template or HTML and put it in a header script tag like

  5. 25/07/2011Gabriele says:

    Excuse me, I amn’t an expert and I don’t understand your suggestion. Can you help me with an example? I run your script and I obtain a concatenated $scriptfile in temp dir (53de55d5d61fbfe2b4dfcd624d1bf489.js, for example, with three google adense javascripts concatenated togheter). I run also: $scriptfile = App_Minifier::fetch (‘.’, ‘temp’, $scripts); but I don’ see any result. How can I put the three different google adsense javascripts called from the concatenated $scriptfile in different positions in html page? Thank you.

  6. 25/07/2011Chris Renner says:

    I think you’re improperly using my class. Since the resulting output file is a merging of all the input js files, you can’t include bits and pieces of it in-line in your code. You have to include it with a header tag like I mentioned above.

    Google Adsense code needs to be inline in your HTML pages, so I don’t think my class here is an appropriate way to include the adsense javascript.

  7. 25/07/2011Gabriele says:

    For header tag do you mean something like put between … ? What kind of concatenated javascript files are good in this case, since the most javascript files should be executed and shown on monitor all togheter? Do you know if there is a solution for my case in order to reduce the http requests of various javascript files? Thank you.

  8. 25/07/2011Gabriele says:

    For header tag do you mean something like <script type=”text/javascript” src=”path/file.js”></script> put between <head> … </head> ? What kind of concatenated javascript files are good in this case, since the most javascript files should be executed and shown on monitor all togheter? Do you know if there is a solution for my case in order to reduce the http requests of various javascript files? Thank you.

  9. 25/07/2011Chris Renner says:

    Concerning header tags, see This tutorial. Please note you have three options: 1) place the actual javascript in header inside a script tag, 2) place the javascript directly in line in the html body (also in a script tag), or use a script tag to remotely include the javascript (which is why my class above does). The above class is not for in-line javascript at all, it is for including the files by reference, the browser initiates a separate HTTP request to retrieve the javascript.

  10. 25/07/2011Chris Renner says:

    Here, read this tutorial in its entirety as this directly explains what to do with the outputted file my classs generates.

  11. 10/08/2011Chella says:

    Hi,

    This tutorial helps me lot to minify js files to one. I would like to know how the gzip the minified files to my end. Could u plz help for this request.

    Thanks,

    Chella.

  12. 4/10/2011Steps says:

    I use “Miser” (sourceforge.net/projects/miser/) for all my sites. It scrapes the page and caches minified JavaScript AND CSS. The cool thing about it is it only minfieis the files when there is a change rather than periodicly.

    It also does other optimizations like replace local javascript with CDNs and moving stuff to the foot of the page. It made my Yslow score go from a poultry 64 to 95.

  13. 5/10/2011Chris Renner says:

    My solution above also only minifies when changes are made the the source files, not periodically.

  14. 7/07/2013Bill says:

    I prefer the easy method of just using an online tool to compress my JS files. My JavaScript online minify tool of choice is http://www.blimptontech.com It uses UglifyJS and I can combine all my files into a single java script file while minifying.

Write a comment: