Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling Image Caching with CakePHP (and Asset Compress)

From Yahoo!'s Best Practices for Speeding Up Your Web Site document:

Expires headers are most often used with images, but they should be used on all components including scripts, stylesheets, and Flash components.

I follow the above advice using the "mod_expires" Apache module. My implementation is much like HTML5 Boilerplate's. See this .htaccess code.

Here is another quote from the same Yahoo! document:

Keep in mind, if you use a far future Expires header you have to change the component's filename whenever the component changes. At Yahoo! we often make this step part of the build process: a version number is embedded in the component's filename, for example, yahoo_2.0.6.js.

I have taken care of this with my CSS and JavaScript files using Mark Story's Asset Compress plugin. It's just a matter of making Asset Compress' shell part of the build process.

Now for the two issues I've run into, both related to images:

I have regular <img> tags throughout my websites and I also have CSS background-images. I currently don't have an elegant way of handling cache busting for either of those two types of images. For <img> tags, I have this line in my "core.php" file:

Configure::write('Asset.timestamp', 'force');

Although this does provide a way of automatically handling cache busting for <img> tags (provided the tags are generated using $this->Html->image(...)), I don't consider this to be elegant for two reasons:

  1. It uses a query string, which is not recommended.
  2. The image's timestamp is checked every time that particular view is accessed. Yes, you could cache the view, but you may want the image(s) in that view to be updated before the cached version of the view expires, so you would have to do whatever is needed to trigger that view to be re-cached, which I don't consider to be elegant.

As for handling the cache busting of CSS background-images, I have to manually update the LESS file. Definitely not elegant.

How is image caching supposed to be handled with CakePHP and/or Asset Compress?

like image 870
Nick Avatar asked Oct 22 '22 06:10

Nick


1 Answers

Cache Invalidation, performance and the web

It's commonly held that one of the hardest things to do in programming is cache invalidation. However with assets (js files, css files, images etc.) that's not really true optimal logic for serving web assets is:

  • Serve with long cache expiry (1 year)
  • Don't use etags
  • If the asset changes change the url

There is however one complication when applied to the web.

Consider a request for /css/main.css, containing:

body {
    background-image:url('../img/bar.gif');
}

This will, obviously, trigger a request for /img/bar.gif when the css file is loaded. Assuming the image is served with appropriate headers, there are only two ways to load an updated version of bar.gif:

  • change the contents of the css file
  • change the folder where the css file is

The first is problematic if it's not automated (and even if automated, could go wrong) the second is easy.

Version-prefix asset urls -> never have problems again

One simple way to solve the css/js/files problem is to make your build number part of the url:

/v123/css/foo.css
 ^

You can do this by modifying your app helper webroot function for example:

public function webroot($file) {
    $file = parent::webroot($file);
    if (Configure::read('debug')) {
        return $file;
    }

    return '/' . Configure::read('App.version') . $file;
}

Incidentally it's the same technique to use a cdn - probably the best thing you can do to improve frontend performance.

In this way when you bump your site version - all assets get new urls. Note that using this technique, all referenced assets need to use relative urls, not absolute:

.foo {
    /* background-image:url('/img/bar.gif'); // won't work */
    background-image:url('../img/bar.gif');
}

Otherwise the request for the css file is application-version specific but the referenced image is not and would be read from browser cache (if relevant) even with a new application version.

Achieving the same result, no changes to file system

You can use a rewrite rule similar to the one in h5bp's for filename cache-busting if you don't want to change your folder structure:

<IfModule mod_rewrite.c>
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^/v\d+/(css|files|js)/(.+)$ /$1/$2 [L]
</IfModule>

This will mean that the url /v123/css/main.css serves the contents of /css/main.css at the time of the request.

Parsing css files is complex

You mention in a comment

I think the fact that changing one asset causes all assets to be re-downloaded is a deal-breaker

Unless you are releasing a new production version every minute - it's not going to be a problem (unless you have GB of cached content in which case .. you have different problems). The only way to have performant cache logic that is file specific is to e.g. store each file in your site as the sha1 of the file's contents - applied to css files that means replacing ../img/foo.gif with ../img/<hash of foo.gif's contents>.gif.

There's nothing to stop using multiple techniques, for example with the following structure:

app
    webroot
        css
            img <- css assets only
        fonts
        img
        js

You can version-prefix your css, fonts and js requests; indirectly do the same for css-images (assuming they use relative urls of the form background-image:url('img/bar.gif');) without applying the same logic to other assets (user avatars, their uploaded cat videos, whatever).

Or use data uris for all images

It's what google does =).

At the end of the day it becomes a choice between how complex you want your build process to be, and for what real benefit. Many users have empty browser caches, so it's quite likely that for a random user the cache logic of an application will only apply to their current visit - one of the main reasons why expiring your whole asset cache in one go isn't such a bad thing.

like image 61
AD7six Avatar answered Oct 27 '22 12:10

AD7six