Minifying CSS and Javascript on the Fly

Friday, September 16, 2011

In an ideal world, websites would only include a single minified CSS file, and if required, a single minified javascript file. There are many benefits of doing this including:

There are several plugins out there for various CMS's and frameworks that accomplish this, but I wrote my own version quite awhile back before these were readily available. What follows is the code I wrote to integrate this into my framework.

Getting Started

The first thing you will need are copies of the freely available Minify_CSS_Compressor, Minify_CSS_UriRewriter and JSMin classes. I left the Minify_CSS_Compressor and Minify_CSS_UriRewriter classes as they are, because the naming conventions match the class naming conventions I use in my framework (modeled after Pear's conventions). In my library directory, I created a new sub-directory named Minify, and placed 3 sub-directories inside that: CSS, JS, and Cache (more on this later). I placed the Minify_CSS_Compressor and Minify_CSS_UriRewriter classes inside the Minify > CSS directory, and after renaming JSMin to Minify_JS_Compressor (and all references to the class name within the class) I placed this file inside the Minify > JS directory.

Thanks to these powerful classes, we already now have the ability to minify both css and js files, but obviously we don't want to do this every time one of these files is requested, as it would put an unnecessary strain on the server and slow things down, which is the opposite of the goal. So next I wrote the cache classes. First, here's the abstract base class that goes in Minify > Cache:

The Minify_Cache_Abstract Class

  1. <?php
  2. abstract class Minify_Cache_Abstract
  3. {
  4. protected $files; // array
  5. protected $text;
  6. protected $cacheFilePathHttp;
  7. protected $encodedFilename;
  8. protected $cacheFileExt;
  9. protected $contentType;
  10. public function __construct()
  11. {
  12. $this->setDefaults();
  13. $this->files = array();
  14. $this->text = '<?php
  15. ob_start(\'ob_gzhandler\');
  16. header(\'Content-type: '.$this->contentType.'\');
  17. header(
  18. "Expires: ".gmdate("D, d M Y H:i:s",
  19. (time()+604800))." GMT");
  20. header(
  21. "Last-Modified: ".gmdate("D, d M Y H:i:s",
  22. '.time().')." GMT");
  23. ?>
  24. ';
  25. $this->encodedFilename = false;
  26. }
  27. ######################################## abstract methods
  28. abstract protected function setDefaults();
  29. abstract protected function minify($txt, $serverPathToFile=null);
  30. abstract public function getHtmlTag();
  31. ######################################## core class methods
  32. public function addFile($filePath)
  33. {
  34. $parts = parse_url($filePath);
  35. $serverPath = SITE_ROOT.$parts['path'];
  36. $size = filesize($serverPath);
  37. $mTime = filemtime($serverPath);
  38. $this->files[] = array($serverPath, $filePath, $size, $mTime);
  39. }
  40. public function save($cacheDirectory='/_cache/')
  41. {
  42. $fileName = $this->getEncodedFilename();
  43. $filePathServer = SITE_ROOT.$cacheDirectory.$fileName;
  44. $this->cacheFilePathHttp = SITE_ROOT_URL.$cacheDirectory.$fileName;
  45. if (file_exists($filePathServer)) {
  46. return $this->cacheFilePathHttp;
  47. }
  48. foreach($this->files as $file) {
  49. $txt = file_get_contents($file[1]);
  50. $this->text .= $this->minify($txt, $file[0]);
  51. }
  52. file_put_contents($filePathServer, $this->text);
  53. return $this->cacheFilePathHttp;
  54. }
  55. protected function getEncodedFilename()
  56. {
  57. if ($this->encodedFilename) { return $this->encodedFilename; }
  58. $this->encodedFilename = md5(serialize($this->files)).$this->cacheFileExt;
  59. return $this->encodedFilename;
  60. }
  61. public function getCacheFilePathHttp()
  62. {
  63. return $this->cacheFilePathHttp;
  64. }
  65. }

You might note the use of two constants in this class: SITE_ROOT and SITE_ROOT_URL. In my system, SITE_ROOT is the server path to the website root, i.e. /home/aaron/www/www, and SITE_ROOT_URL is the http (or https) path, i.e. http://www.aaron-fisher.com. Let's take the methods in this class one by one:

  • __construct(): This is going to call the abstract method setDefaults(), which will be defined in the child classes. Outside of that and some variable initialization, the important thing to note is that this is creating a string which will eventually be saved as a PHP file. Both of our final resulting css and js files will be php files, because this way we can specify the "Last-Modified" and "Expires" headers, along with serving the files with gzip compression (all of which save bandwidth and make Google happy). The file will still be served as the appropriate text/css or text/javascript mime type by specifying this in the child classes. You could do this through .htaccess if you are on a Linux box, but hosting providers tend to have different Apache configurations (and sometimes do not even allow .htaccess files to be modified). This method is cross-server compatible.
  • setDefaults(), minify(), and getHtmlTag(): abstract methods which we will see implemented in a bit
  • addFile(): This is the method we will eventually use to add a js or css file into the array of files that need minified. Notice that this collects information about the file, such as size, and modification date. These will be used later to decide if any of the included files have changed since we last created a version.
  • save(): Once all the files have been added to the Cache class, we generate a unique filename for the file (see getEncodedFilename() below). If the encoded file name of all of our includes matches the filename of an existing file we just return the path to it. Otherwise we use file_get_contents() to get the contents of each file, append the minified version of each to a string, and save the entire thing as a php file.
  • getEncodedFilename(): This generates a unique filename for the file through a neat little trick: each time addFile() is called, it stores the path, file size, and modified time of each file in an array. We now serialize() this array creating a unique string based on these file attributes. Because this string contains invalid characters for a file name, we md5() the whole thing and what's left is a filename that is guaranteed to be unique for the parameters of the files we have included.

Now it's time to implement this class for both CSS and JS versions:

The Minify_CSS_Cache Class

  1. <?php
  2. class Minify_CSS_Cache extends Minify_Cache_Abstract
  3. {
  4. protected function setDefaults()
  5. {
  6. $this->cacheFileExt = '.css.php';
  7. $this->contentType = 'text/css';
  8. }
  9. public function getHtmlTag()
  10. {
  11. return '<link rel="stylesheet"
  12. href="'.$this->cacheFilePathHttp.'"
  13. type="text/css" />
  14. ';
  15. }
  16. protected function minify($txt, $serverPathToFile=null, $httpPathToFile=null)
  17. {
  18. $txt = Minify_CSS_UriRewriter::rewrite(
  19. $txt,
  20. dirname($serverPathToFile),
  21. SITE_ROOT
  22. );
  23. return Minify_CSS_Compressor::process($txt);
  24. }
  25. }

As you can see, the majority of the work is done by the abstract parent class. All we are doing here is defining the file extension, mime type, and html tag properties, along with implementing the minify() method. This file is saved to the Minify > CSS directory.

The Minify_JS_Cache Class

  1. <?php
  2. class Minify_JS_Cache extends Minify_Cache_Abstract
  3. {
  4. protected function setDefaults()
  5. {
  6. $this->cacheFileExt = '.js.php';
  7. $this->contentType = 'text/javascript';
  8. }
  9. public function getHtmlTag()
  10. {
  11. return '<script src="'.$this->cacheFilePathHttp.'"
  12. type="text/javascript"></script>
  13. ';
  14. }
  15. protected function minify($txt, $serverPathToFile=null)
  16. {
  17. return Minify_JS_Compressor::minify($txt);
  18. }
  19. }

Not much to say here - just defining the same unique properties for javascript files. This file is saved to the Minify > JS directory.

Implementation

Now we're ready to minify the hell out of some stuff! The implementation is entirely up to you and the constraints of the framework you code in, but here's how I implement this in my MVC framework. I have a View class which is responsible for calling the appropriate templates and passing view variables generated in the model layer to those templates. I created two methods in my View class to set javascript and css includes, which accept two arguments: the path to the javascript or css file, and the file priority. Since these includes can ultimately be called by several different modules, the 'priority' argument provides a way to order the includes, as these files often need to be included in a certain order to function properly.

Example View Class

  1. <?php
  2. class View
  3. {
  4. protected $cssIncludes; // array
  5. protected $jsIncludes; // array
  6. /* snip */
  7. public function addCssInclude($path, $lvl=0)
  8. {
  9. $this->addAsset('cssIncludes', $path, $lvl);
  10. }
  11. public function addJsInclude($path, $lvl=0)
  12. {
  13. $this->addAsset('jsIncludes', $path, $lvl);
  14. }
  15. protected function addAsset($arrayName, $path, $lvl)
  16. {
  17. if (!isset($this->{$arrayName}[$lvl])) {
  18. $this->{$arrayName}[$lvl] = array($path);
  19. } elseif (!in_array($path, $this->{$arrayName}[$lvl])) {
  20. $this->{$arrayName}[$lvl][] = $path;
  21. }
  22. }
  23. public function render($pageTitle, $templatesArray=null)
  24. {
  25. $this->setTemplates($templatesArray);
  26. header('Content-type: text/html; charset=utf-8');
  27. // turn on output buffering
  28. if (ENBALE_OUPUT_BUFFERING) {
  29. ob_start('ob_gzhandler');
  30. }
  31. // js includes
  32. if (empty($this->jsIncludes)) {
  33. $this->setVar('JSINCLUDES', '');
  34. } else {
  35. $cache = new Minify_JS_Cache;
  36. ksort($this->jsIncludes, SORT_NUMERIC);
  37. foreach($this->jsIncludes as $level => $includes) {
  38. foreach($includes as $path) {
  39. $cache->addFile($path);
  40. }
  41. }
  42. $cache->save();
  43. $this->setVar('JSINCLUDES', $cache->getHtmlTag());
  44. }
  45. // css includes
  46. $cache = new Minify_CSS_Cache;
  47. ksort($this->cssIncludes, SORT_NUMERIC);
  48. foreach($this->cssIncludes as $level => $includes) {
  49. foreach($includes as $path) {
  50. $cache->addFile($path);
  51. }
  52. }
  53. $cache->save();
  54. $this->setVar('CSSINCLUDES', $cache->getHtmlTag());
  55. // render templates
  56. foreach ($this->templatesArray as $template) {
  57. $temp = (is_null($template))
  58. ? new Template()
  59. : new Template($template, $this->pathToTemplates);
  60. $temp->title = PAGE_TITLE_PREFIX.$pageTitle;
  61. if (is_array($this->templateVars)) {
  62. foreach ($this->templateVars as $varName => $tmpVar) {
  63. $temp->$varName = $tmpVar;
  64. }
  65. }
  66. $temp->render();
  67. }
  68. }
  69. /* snip */
  70. }

Example Usage:

  1. <?php
  2. $view = new View;
  3. $view->addCssInclude('http://www.example.com/css/site.css');
  4. $view->addCssInclude('http://www.example.com/css/override.css', 20);
  5. $view->addJsInclude('http://www.example.com/js/jQuery.js');
  6. $view->addJsInclude('http://www.example.com/js/my-js.js', 20);
  7. $view->render('My Page');

Final Note

You can view the source of any page on this site to look at the js and css includes that are generated using this method. What's great about this method is that you can still have nicely formatted files to work on, and new minified versions are not created until a file is modified. If you are making many routine changes to a website while employing this method, it might be a good idea to delete old cache files through a cron job, as the cache directory can fill up quickly.

Have fun, and go minify something!

Posted by Aaron Fisher at 1:04pm

2 Comments RSS

# 1 By KS301 on July 17, 2013 - 11:14am

KS301

Great article, you really nailed it on the head as to why people should be doing this and the great benefits from the results. Though I prefer the UglifyJS library for minification needs for JavaScript. It also gives you a little obfuscation due to the shrinking of variables and similar. I find I tend to get a smaller JS file in the end with UglifyJS than I do using JSMin. Some do not see a huge difference, only a couple percent, but there are other times I have seen as much as a 12% difference in file size. Never ran into any problems either with the obfuscation that takes place with UglifyJS.

My favourite free online tool is at www.BlimptonTech.com they also allow you to combine multiple JS files into a single source.


# 2 By tonytong on December 30, 2015 - 2:24am

tonytong

i find a free online service to minify js http://www.online-code.net/minify-js.html and compress css http://www.online-code.net/minify-css.html, so it will reduce the size of web page.


Login To Post A Comment

Don't have an account yet? Sign Up