PHP: Combine and compile CSS and JS into Gzipped files

This has actually been the secret behind a lot of our great system performance, but as it was open source originally, it's going back to the people. Its a relatively straightforward script and speeds up JS and CSS performance by a huge amount, especially if you are using multiple files (like we do!).

It should be noted that the compression and combining engine is standard on all hosting accounts - just skip straight to step 5.

The red settings represent paths that need to be set dependant on where you put the files.

It is based on the combine script by Rakaz (http://rakaz.nl/item/make_your_pages_load_faster_by_combining_and_compressing_javascript_and_css_files), but there were notable flaws in that version.

  • gZip encoding was not handled correctly by the browser
  • cache files build up (requires cron to remove routinely)
  • content echo'ing and incorrect headers causing slowdowns

Our version is about 100-200% faster than the original build, we thank Rakaz for giving us the chance to build it.

New features include:

  • automatic update of cache files when a file has been changed
  • automatic generation of gzipped and non gzipped files (for browsers which do not support gzip)
  • optimised headers
  • independent JavaScript library support
  • no CPU overhead of gzipping on the fly as the gzipped version is permanently stored the first time a visitor hits a page which hasn't been cached
Get a feel for the difference here:

(left) gZipped JavaScript includes: https://www.sonassi.com/index/

(right) Non-gzip JavaScript includes: https://www.sonassi.com/index-dummy/

Lets get going then

Pre-requisites: Enable mod_rewrite
# a2enmod rewrite
# apache2ctl restart
And mod_expires
# a2enmod expires
# apache2ctl restart
And mod_deflate
# a2enmod deflate
# apache2ctl restart

You need to edit a few files for the next stage:

apache2.conf
.htaccess
combine-gzip.inc.php

1 ) In the apache2.conf or .htaccess, add the following lines

RewriteRule ^/gzipjs/(.*.js) /data/lib/php/combine-gzip.inc.php?type=javascript&encoding=gzip&files=$1
RewriteRule ^/gzipcss/(.*.css) /data/lib/php/combine-gzip.inc.php?type=css&encoding=gzip&files=$1
 

  RewriteCond %{HTTP_USER_AGENT} ..*Safari.*. [OR]
  RewriteCond %{HTTP:Accept-Encoding} !gzip
  RewriteRule (.*).combine-cache.gzip$ $1.combine-cache [L]
 
  #AddType "text/plain" .gzip
  AddEncoding gzip .gzip

 

  SetEnvIfNoCase Request_URI
    .(?:exe|t?gz|zip|bz2|sit|rar|gzip)$
    no-gzip dont-vary

2 ) For a spot of extra performance, enable mod_expires, and enter this into your apache conf

  ExpiresActive On
  ExpiresDefault A300
  ExpiresByType text/html A7200
  ExpiresByType text/javascript A604800
  ExpiresByType application/x-javascript A604800
  ExpiresByType text/css A604800
  ExpiresByType image/x-icon A31536000
  ExpiresByType image/gif A604800
  ExpiresByType image/jpg A604800
  ExpiresByType image/jpeg A604800
  ExpiresByType image/png A604800
  ExpiresByType text/plain A604800

3) Then create the cache directory in your document_root
# cd DOCROOT
# mkdir ./cache
# chmod 775 ./cache
4) Create the combine-gzip.inc.php, with the following content
$_SERVER['DOCUMENT_ROOT'] . 'cache';
  $cssdir   = $_SERVER['DOCUMENT_ROOT'] . 'css';
  $jsdir    = $_SERVER['DOCUMENT_ROOT'] . 'js';
  $allowed_encoding = array('none','gzip','deflate');
  $js_library = '/data/lib/js'; # No trailing slash

  if(isset($_GET['encoding']) && in_array($_GET['encoding'],$allowed_encoding))
    $encoding = $_GET['encoding'];

  // Determine the directory and type we should use
  switch ($_GET['type']) {
    case 'css':
      $base = realpath($cssdir);
      break;
    case 'javascript':
      $base = realpath($jsdir);
      break;
    default:
      header ("HTTP/1.0 503 Not Implemented");
      exit;
  };

  $type = $_GET['type'];
  $elements = explode(',', $_GET['files']);

  // Determine last modification date of the files
  $lastmodified = 0;
  while (list(,$element) = each($elements)) {
    if (strstr($element,'/lib/')) {
      $element = str_replace('/lib',$js_library,$element);
    } else {
      $element = $base . '/' . $element;
    }
    $path = realpath($element);

    if (($type == 'javascript' && substr($path, -3) != '.js') ||
      ($type == 'css' && substr($path, -4) != '.css' &&
      (substr($path, 0, strlen($base)) != $base || substr($path, 0, strlen($js_library)))
      )) {
      header ("HTTP/1.0 403 Forbidden");
      exit;
    }

    if (!file_exists($path)) {
      header ("HTTP/1.0 404 Not Found");
      exit;
    }

    $lastmodified = max($lastmodified, filemtime($path));
  }

  // Send Etag hash
  $mhash = md5($_GET['files']);
  $hash = $lastmodified . '-' . $mhash;
  header ("Etag: "" . $hash . """);

  if (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
    stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) == '"' . $hash . '"')
  {
    // Return visit and no modifications, so do not send anything
    header ("HTTP/1.0 304 Not Modified");
    header ('Content-Length: 0');
  }
  else
  {
    // First time visit or files were modified
    if ($cache)
    {
      // Determine supported compression method
      $gzip = strstr($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip');
      $deflate = strstr($_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate');

      // Determine used compression method
      if ($encoding != 'none')
        $encoding = $gzip ? 'gzip' : ($deflate ? 'deflate' : 'none');

      // Check for buggy versions of Internet Explorer
      if (!strstr($_SERVER['HTTP_USER_AGENT'], 'Opera') &&
        preg_match('/^Mozilla/4.0 (compatible; MSIE ([0-9].[0-9])/i', $_SERVER['HTTP_USER_AGENT'], $matches)) {
        $version = floatval($matches[1]);

        if ($version < 6)
          $encoding = 'none';

        if ($version == 6 && !strstr($_SERVER['HTTP_USER_AGENT'], 'EV1'))
          $encoding = 'none';
      }

      // Try the cache first to see if the combined files were already generated
      $plaincachefile = 'cache-' . $hash . '.' . $type . '.combine-cache';
      $cachefile = $plaincachefile . ($encoding != 'none' ? '.' . $encoding : '');

      if (file_exists($cachedir . '/' . $cachefile)) {

        if ($encoding=='gzip') {
          header("HTTP/1.1 301 Moved Permanently");
          header("Cache-Control: access plus 7 days");
          header("Location: /cache/$cachefile");
          header("Connection: close");
          exit;
        } else {
          header("HTTP/1.1 301 Moved Permanently");
          header("Cache-Control: access plus 7 days");
          header("Location: /cache/$plaincachefile");
          header("Connection: close");
          exit;
        }
      }
    }

    // Get contents of the files
    $contents = '';
    reset($elements);
    while (list(,$element) = each($elements)) {
      if (strstr($element,'/lib/')) {
        $element = str_replace('/lib',$js_library,$element);
      } else {
        $element = $base . '/' . $element;
      }

      $path = realpath($element);
      $contents .= "nn" . file_get_contents($path);
    }

    // Send Content-Type
    header ("Content-Type: text/" . $type);

    // Store cache
    if ($cache) {
      if ($encoding != 'none') {
        $gcontents = gzencode("/*nGZIPPED $mhashn*/" . $contents, 9, $gzip ? FORCE_GZIP : FORCE_DEFLATE);

        if ($fp = fopen($cachedir . '/' . $cachefile, 'wb')) {
          fwrite($fp, $gcontents);
          fclose($fp);
        }
      }
      if ($fp = fopen($cachedir . '/' . $plaincachefile, 'wb')) {
        fwrite($fp, $contents);
        fclose($fp);
      }

      foreach (glob($cachedir . "/" . "*" . $type .".combine-cache*") as $filename) {
        if (strstr($filename,$mhash) && !strstr($filename,$cachefile) && !strstr($filename,$plaincachefile))
          @unlink($filename);
      }
    }

    if ($encoding=='gzip') {
      header("HTTP/1.1 301 Moved Permanently");
      header("Location: /cache/$cachefile");
      header("Connection: close");
      exit;
    } else {
      header("HTTP/1.1 301 Moved Permanently");
      header("Location: /cache/$plaincachefile");
      header("Connection: close");
      exit;
    }
  } 

?>
5) Add the path into your tags with comma separated values, Eg.
<link rel="stylesheet" type="text/css" href="/gzipcss/style.css,nav.css,blog.css" title="default" />
<script type="text/javascript" src="/gzipjs//lib/jquery.min.js,boss-site-search.js,/lib/superfish.js" />
Then sit back and relax enjoying your MASSIVELY reduced bandwidth and CPU overheads.

For examples of this script in use, check out any page of our website at www.sonassi.com

[syntaxhighlighter]