Reduce the Size of Your Javascript

Last week, while using YSlow to check the performance of one of my Web sites, I finally decided to look into the advice it gives about minifying my javascript. With me using jQuery and some of its plugins, there were a few pages on which I was including four or five separate external javascript files (the jQuery library, the jQueryUI library, one or two plugins and my own javascript functions), and only some of them were minified.

So, I took to the Web to see what resources were available in PHP that I might be able to integrate with my custom CMS. That’s when I came across Valentin Agachi’s javascript packing package.

In the package, Valentin includes his own PHP port of Dean Edwards’ Packer (this version was ported from the .NET version rather than using PHP port of Packer available on Dean’s Web site) and a copy of Douglas Crockford’s JSMin.

The package is set up to be called dynamically by your root .htaccess file, so it’s extremely easy to implement.

However, in my case, I wanted to combine my files (along with any incidental inline javascript I used) into one before packing them. Therefore, I wrote a class for my CMS that searches for “script” tags, pulls the necessary information out, consolidates all of the script into a single file and then invokes the Packer functions. I’ve included that code below, in case you want to try using it (hopefully WordPress won’t mess anything up).

/**
 * Define the absolute path to the file we will be using to pack our javascript
 */
define('PACKERPATH','/var/www/scripts/js-size/ECMAPacker.php');
/**
 * Define the absolute path to the root of your Web site
 */
define('ROOTPATH','/var/www/');
/**
 * Define the URL to the root of your Web site.
 */
define('SITEURL','http://www.example.com/');
/**
 * Define the URL (relative to your root domain) to the root of your Web site. If you Web site is not in a sub-directory, you can leave this defined as a single slash
 */
define('SHORTURL','/');

/**
 * CAGCMS Script Class
 *
 * Consolidates javascript, building a single compressed/minified file for all javascript actions to be used on a page
 */
class cagcmsscript {
	/**
	 * The HTML-accessible location into which to save the new file
	 */
	public $location = NULL;
	/**
	 * The absolute path to the new file you want to save
	 */
	public $path = NULL;
	/**
	 * The source code we will be searching for javascript calls
	 */
	public $html = '';
	/**
	 * An empty variable set up to store all of the code that will be output to our new file
	 */
	public $code = '';
	/**
	 * Indicates whether to save the newly consolidated code into an external file or just to return it inline
	 */
	public $save = true;
	/**
	 * An empty variable to store the most recent modification date of the script files found in our HTML
	 */
	private $updated = NULL;

	/**
	 * Builds our new lfscript object
	 * @param array $av An associative array of our properties and values
	 */
	function __construct($av=array()) {
		foreach($av as $k=>$v) {
			if(property_exists($this,$k)) {
				$this->$k = $v;
			}
		}
	}

	/**
	 * Build our new file
	 *
	 * Build the new javascript file and save it to the appropriate place on the server.
	 * @param bool $pack Indicates whether the output should be packed or not
	 * @uses ECMAScriptPacker::Pack()
	 * @return string The HTML call to the new javascript file
	 */
	function buildScriptFile($pack=false) {
		if(empty($this->html)) {
			return false;
		}
		if($this->parseScriptFile()) {
			if($pack) {
				require_once(PACKERPATH);
				$js = new ECMAScriptPacker;
				$this->code = $js->Pack($this->code);
			}
			if($this->save) {
				if(is_null($this->updated) || $this->updated >= filemtime($this->path)) {
					file_put_contents($this->path,$this->code);
				}
				$this->html = '<script type="text/javascript" src="'.$this->location).'"></script>';
			}
			else {
				$this->html = '<script type="text/javascript">'.$this->code.'</script>';
			}
		}
		return $this->html;
	}

	/**
	 * Find and parse Javascript
	 *
	 * Find all of the src calls and pull the content of those files.
	 * Find all of the inline javascript and add that in the appropriate place.
	 * @return bool Whether the script was parsed successfully or not
	 */
	function parseScriptFile() {
		if(preg_match_all('#<script((\s(type|src|language)=("|\')([^\4]+?)\4)+?)>(.*?)</script>#',$this->html,$matches,PREG_SET_ORDER)) {
			foreach($matches as $m) {
				if(preg_match('#src=("|\')([^\1]+?)\1#',$m[1],$src)) {
					$src = $src[2];
					$internal = true;
					if(stristr($src,SITEURL)) {
						$src = str_replace(SITEURL,ROOTPATH,$src);
					}
					elseif(substr($src,0,strlen(SHORTURL)) == SHORTURL) {
						$src = ROOTPATH.substr($src,(strlen($GLOBALS['paths']['shorturl'])-1));
					}
					else {
						$internal = false;
					}
					$this->updated = (is_null($this->updated) || $this->updated <= filemtime($src)) ? filemtime($src) : $this->updated;
					$this->code .= file_get_contents($src);
				}
				elseif(!empty($m[6])) {
					$this->code .= $m[6];
				}
				/**
				 * Uncomment the block below if you want to also search for jQuery AJAX calls to external script files and consolidate them into your new file.
				 * This block of code does contain some proprietary code for my CMS, so you may have to make some adjustments to get it to work for you.
				 */
				/*if(preg_match_all('#\$\.getScript\((.+?)\);#',$this->code,$ajaxcalls,PREG_SET_ORDER)) {
					foreach($ajaxcalls as $a) {
						$a[1] = str_replace(array('+','\'','"'),'',$a[1]);
						$a[1] = str_replace(SITEURL,ROOTPATH,$a[1]);
						$this->code = str_replace($a[0],file_get_contents($a[1]),$this->code);
					}
				}*/
				$this->html = str_replace($m[0],'',$this->html);
			}
			return true;
		}
		return false;
	}
}