Pressure is nothing more than the shadow of great opportunity. - Michael Johnson

Automatic versioning of CSS, JavaScript and Images

6 years, 4 months ago

See comments for important information about HTTP caching issues with URLs containing query string parameters.

When including CSS and JavaScript resources in your pages you should version file paths and update these version numbers every time the files change. This is necessary as a visitor's browsers may, depending on settings, continue to cache files even after a change. This can result in a mismatch between your HTML and these external resources which may cause rendering or functionality problems. One will likely encounter similar caching issues with images.

So if we work on the basis that we should version these resources and that changing version numbers forces the browser to reload those resources then we can actually gain large performance improvements by explicitly instructing the browser to cache them for an extended period of time (say 10 years) thereby limiting the number of times the browser goes back to check for fresh copies.

This can be achieved fairly easily within the virtual host settings of your Apache conf file. Simply add the following directives within the relevant virtual hosts section substituting /assets/ with the file path corresponding to the location where your CSS, JavaScript and images are stored.

  1. <Location /assets/>
  2. ExpiresActive On
  3. ExpiresDefault "access plus 10 years"
  4. </Location>

If you have the mod_deflate module installed then you can gain an additional performance benefit by gzipping CSS and JavaScript resources to reduce the amount transmitted down the wire.

  1. <Location /assets/>
  2. ExpiresActive On
  3. ExpiresDefault "access plus 10 years"
  4. SetOutputFilter DEFLATE
  5. </Location>

These directives can also be added to an appropriately placed .htaccess file but as Stuart explains there are performance implications with this method so it should only be chosen if you don't have access to your Apache conf files (perhaps because you're using shared hosting).

Applying both these settings covers off a couple of recommendations made by Yahoo!'s YSlow performance testing tool so not only will you end up with a faster site but you'll also improve your YSlow rating by a grade or two.

Of course manually versioning files is a pain, it's repetitive and as Stuart is always reminding me manually repetitive tasks should be eliminated wherever possible. In the past I wrote a script which automatically merges CSS or JavaScript files and versions the combined output. Here's some simpler code which versions individual files based their last modified date - every time the file is updated it's version number updates accordingly.

  1. <?php
  2. class Version {
  3. private static $aLookup = array();
  4. public static function Get($sFilename) {
  5. if (!array_key_exists($sFilename, self::$aLookup)) {
  6. $sRealPath = realpath($_SERVER['DOCUMENT_ROOT'] . "/$sFilename");
  7. if (file_exists($sRealPath) && ($iTimestamp = filemtime($sRealPath))) {
  8. self::$aLookup[$sFilename] = $iTimestamp;
  9. $sFilename .= "?v=$iTimestamp";
  10. }
  11. } else {
  12. $sFilename .= '?v='.self::$aLookup[$sFilename];
  13. }
  14. return $sFilename;
  15. }
  16. public static function GetLink($sFilename) {
  17. return '<link rel="stylesheet" type="text/css" href="'.self::Get($sFilename).'">';
  18. }
  19. public static function GetScript($sFilename) {
  20. return '<script type="text/javascript" src="'.self::Get($sFilename).'"></script>';
  21. }
  22. public static function GetImage($sFilename, $iWidth, $iHeight, $sAlt = '') {
  23. return sprintf('<img src="%s" width="%d" height="%d" alt="%s">', self::Get($sFilename), $iWidth, $iHeight, $sAlt);
  24. }
  25. }
  26. ?>

Download plain text version (Updated version to fix HTTP caching issues)

Using it is pretty simple - wrap the required function around the file path you'd normally have supplied to link, script or img tags.

  1. <link rel="stylesheet" type="text/css" href="<?php echo Version::Get('/css/dark.css'); ?>">

In fact it's even easier than that - I've also provided some wrapper functions which make outputting link, script and img tags a little bit easier. The example above could be written more simply as follows:

  1. <?php echo Version::GetLink('/css/dark.css'); ?>

Comments

  • Ahh, this is just what I was looking for, I looked at your script that merges and automatically versions css and js earlier, but I couldn't use that as I include different css for the various versions of IE (I don't like css hacks).

    Thanks for sharing, I will definitely use this, and leave another comment about my experience...

    Adriaan Nel - 7th April 2008 #

  • I'm sure you can make it even shorter, memoization shouldn't be that verbose.

    public static function Get($sFilename) {
     if (!array_key_exists($sFilename, self::$aLookup)) {
      $sRealPath = realpath($_SERVER['DOCUMENT_ROOT'].'/'.$sFilename);
      self::$aLookup[$sFilename] = file_exists($sRealPath) ?
       filemtime($sRealPath) :
       -1;
     }
     return $sFilename.'?v='.self::$aLookup[$sFilename];
    }

    I'm just wondering how does realpath behave on Windows? http://uk.php.net/manual/en/function.realpath.php#72416

    Your examples are using absolute path to css/js files. Is the '/' useful then?

    Cheers,

    -- Yoan

    PS: very cool captcha.

    Yoan - 8th April 2008 #

  • As I just said over on Stuart's blog, I believe that file paths with query strings should not be cached as per the HTTP spec, so you're safer versioning the filename rather than the request path, so something like (untested):

    $sFilename = preg_replace('/^(\/(.*?))\.(css|js|jpg|gif)$/', '\\1.'.$iTimestamp.'.\\3', $sFilename);

    and the following in your httpd.conf:

    RewriteRule ^/static/([a-z-]+)\.[0-9]+\.(css|js) /static/$1\.$s [L]

    Which rewrites the above file name of "/static/file.1207433992.css" or similar to "/static/file.css" on your local box.

    Note: Apache conf untested, but you get the idea. :)

    Brad Wright - 8th April 2008 #

  • Sorry, my mod_rewrite has fail:

    RewriteRule ^/static/([a-z-]+)\.[0-9]+\.(css|js) /static/$1\.$2 [L]

    is better.

    Also my regex had a bit of optimisation fail:

    $sFilename = preg_replace('/^(\/.*?)\.(css|js|jpg|gif)$/', '\\1.'.$iTimestamp.'.\\2', $sFilename);

    Has one less capture (thanks Stuart for coming to my desk and teaching me the ways).

    Brad Wright - 8th April 2008 #

  • @Yoan - thanks for posting shortened code. It needs slight changes to prevent it writing v=-1 in situations where it's not able to read the file but other than that much improved.

    I added the extra slash (/) because, although I've specified absolute paths others may not. Where realpath encounters 2 slashes next to each other it'll reduce to one so the additional one isn't a problem in the case where absolute paths are specified.

    I'm not sure about realpath on Windows - certainly PHP on Windows has lots of issues so I wouldn't recommend using it unless one absolutely has to. Linux based hosting is usually cheaper anyway so unless one has to run alongside other Windows based systems, connect to SQL Server databases or you're developing on a Windows based development box this shouldn't be an issue.

    @Brad - thanks for pointing this out. Umm, my function now has fail I guess. Given the problems you highlight it would definitely be better to throw in an additional rewrite rule to allow the version information to be written before the extension.

    I've rolled Yoan's suggestions and code and yours into a new function:

    public static function Get($sFilename) {
       if (!array_key_exists($sFilename, self::$aLookup)) {
          $sRealPath = realpath($_SERVER['DOCUMENT_ROOT'].'/'.$sFilename);
          self::$aLookup[$sFilename] = file_exists($sRealPath) ? '.'.filemtime($sRealPath) : '';
       }
       return preg_replace('/^(\/.*?)\.(css|js|jpe?g|gif)$/', '\\1'.self::$aLookup[$sFilename].'.\\2', $sFilename);
    }

    Download amended class as plain text

    The corresponding regular expression you'll need to add to your Apache conf (or .htaccess file) is:

    RewriteRule ^/static/(.*?).[0-9]+.(css|js|jpe?g|gif|png) /static/$1.$2 [L]

    Where "static" is the directory containing your resources.

    Ed Eliot - 8th April 2008 #

  • For a Python version see Stuart's version of this code.

    Ed Eliot - 8th April 2008 #

  • My comment is more in line with what Adriaan has commented on. that is my inclusion of "different css". I am not an expert but I also like to avoid hacks at all costs. I am going to give what you have here a shot and see what happens. Yoan great spot. I am working hard on getting to the level of expertise I see here but it seems there is some real talent above. I will be back with questions I am sure. Thank you for this!

    mary - 8th April 2008 #

  • An update since this was first posted and discussed:

    Over on the YUI blog Yahoo! posted about their service for combining JS resources. For this service they do use URLs with query string components and as Eric explains in reply to my comment browsers will cache these URLs as long as expires headers are set.

    In the post above these expires headers are set so it seems likely the original technique described here and Brad's proposed changes would both work fine.

    Ed Eliot - 11th September 2008 #

  • This is nice...

    But what if using smarty templates there you can't use

    01.

    aykak - 27th April 2011 #

  • yazılım hocası

    Yazılım hocası - 28th April 2013 #

Help make this post better

Notes: Standard BBCode for links, bold, italic and code are supported. rel="nofollow" is added to all links. Your email address will never be displayed on the site.

Back to index