Simple CSS/JS Concatenation and Versioning with PHP

Here’s a quick and easy way way to concatenate stylesheets and JavaScript using PHP. I’ll also show how to layer on file versioning so you can use far future expires headers for optimal client side caching.

There are many ways to combine files to reduce HTTP requests–you can use a build tool to prepare static assets as part of a build process or you can combine files on-the-fly using Apache mods, Server Side Includes (SSI), or other server side solutions, including CMS plugins. The right solution of course depends on your project, team, and environment. If a build step is not the right fit for your project and you want to implement something lightweight without having to fiddle with Apache too much, you may find this useful.

I’ll mention that this is a very simple technique and something that developers have been doing for years. I’m simply documenting it so that it has a place in our collective performance toolkit.

all.js.php

To get started we create a PHP file called all.js.php and then:

  1. Set the Content-type header to specify either JS or CSS
  2. List our scripts/stylesheets in the order we want them included
  3. Loop through the array and include each file

The code looks like this:

<?php
header('Content-type: application/javascript');
 
$files = array(
  //Plugins
  dirname(__FILE__) . '/plugins/jquery.hoverIntent.min.js',
  dirname(__FILE__) . '/plugins/jquery.placeholder.min.js',
  dirname(__FILE__) . '/plugins/jquery.tweet.min.js',
 
  //Core scripts
  dirname(__FILE__) . '/core.js',
 
  //Other scripts
  dirname(__FILE__) . '/main.js'
);
 
foreach ($files as $file) {
  include($file);
}
?>

Then you just reference the all.js.php file in your document and it returns all of your JS as a single script.

The obvious advantage of concatenating on-the-fly is that you don’t have to add a build step to your workflow. The benefits of using PHP over server side includes are that (a) it’s likely to be more accessible to developers on your team and (b) you can make use of application variables in your asset list. This is very handy for pulling in shared files (e.g., libraries and plugins) from a centralized location.

Is performance a concern? It depends. If you have an under-provisioned server and you’re trying to concatenate dozens of big files under heavy traffic load, it’s possible you’ll experience some performance degradation. But in most cases I don’t see it being an issue. PHP is fast. Regarding response time, ApacheBench tests I’ve done in the past revealed no significant difference between a pre-concatenated file and one concatenated on-the-fly with PHP.

But here’s the main reason I don’t think performance is an issue: if performance and load are concerns for your site, you’ll already have some layer of caching in place. Maybe it’s memcache, maybe it’s Varnish, maybe it’s a CDN. There’s no reason your concatenated file would be generated with each request — it’s going to end up sitting in a cache anyway.

Cache headers and versioning

When you dynamically combine files you can run into problems with behavior based on the file’s last modified date. For instance if you’re using Server Side Includes, by default Apache does not send the last modified header.

To get around this we can use expires headers to give the combined file an explicit expiration date. Even better, we can use far future expiration dates so that browsers continue to use a cached copy of the file until we make changes. All we have to do is implement some form of file versioning to let clients know when to request a new copy of the file.

There are two parts to it: the PHP in your templates that adds a version number to each CSS and JS file reference, and an Apache rewrite rule that rewrites the versioned the filename to the canonical filename. It’s really simple.

(I’m not going to cover how to set cache control expires headers since it’s well documented elsewhere. If you need a reference, I recommend looking at HTML5Boilerplate’s .htaccess file.)

Adding version numbers to CSS/JS references

Either in your main layout file or wherever in your app you store global config, declare a variable that represents the current version number for your static assets:

$static_version = '1';

Also in your main layout file or wherever you store global functions, add this function that accepts a file path and injects a version number before the file extension:

function version_stamp($url) {
  $name = explode('.', $url);
  $lastext = array_pop($name);
  array_push($name, $static_version, $lastext);
  return implode('.', $name);
}

This will turn all.js.php references into all.js.1.php. The reason the version number is built into the filename as opposed to being append as a query parameter is that some proxies will not cache files containing query strings.

Then we modify our JS and CSS references to include the version stamp function like this:

<script src="<?php echo version_stamp('/_assets/js/all.js.php') ?>"></script>
Adding Apache rewrite

Adding the following rewrite rule to .htaccess or httpd.conf will route requests for all.js.1.php to all.js.php.

# File versioning
RewriteRule ^(.+)\.(js|css).([0-9]+)\.(php)$ $1.$2.$4

Then whenever you make JS or CSS updates you just increment the version number in the $static_version variable and browsers will know to request the fresh copy.

Is this the right solution?

If a build process makes sense for your workflow and if you can integrate a build process without incurring the overhead of something like Ant, I think a build tool is often the better solution. If a build step is inconvenient for your workflow, the various on-the-fly options are probably equal. (I’m sure someone will comment that concatenating with Apache is faster than PHP, and while that may be technically true, practically speaking it may be an over optimization. And as I mentioned above, a proper caching layer renders the argument moot.)

The advantages to this method are that it’s simple, easy to implement, and all done in PHP (aside from the apache rewrite rule).

Lastly, if you’re interested in this stuff make sure to read Robert Nyman’s Tools For Concatenating And Minifying CSS And JavaScript Files In Different Development Environments. It’s a great round-up of tools in a variety of languages and frameworks.

Both comments and pings are currently closed.

Discussion

I guess it`s better to save those several includes and automatically create a static JS file which will be minimized and comment stripped too. If you than have a all.js as a static file you can use the rewrite part to open it as all..js less work for the IO scheduler :)

I’m sure this is a silly question but what would the effects be of a page being served to a user without js enabled?

Nice about the basics, but may I suggest looking at Assetic for exactly these needs? Handles file concatination, precompiling (think LESS for CSS or CoffeeScript for JS) and filtering. Dumping statically or generating on the fly. I came to love it…

@Waylon – The same as always; the JS won’t execute. Not entirely sure if it will still be downloaded or not.