views:

286

answers:

11

Hi,

If I understand correctly, a broswer caches images, JS files, etc. based on the file name. So there's a danger that if one such file is updated (on the server), the browser will use the cached copy instead.

A workaround for this problem is to rename all files (as part of the build), such that the file name includes an MD5 hash of it's contents, e.g.

foo.js -> foo_AS577688BC87654.js
me.png -> me_32126A88BC3456BB.png

However, in addition to renaming the files themselves, all references to these files must be changed. For exmaple a tag such as <img src="me.png"/> should be changed to <img src="me_32126A88BC3456BB.png"/>.

Obviously this can get pretty complicated, particularly when you consider that references to these files may be dynamically created within server-side code.

Of course, one solution is to completely disable caching on the browser (and any caches between the server and the browser) using HTTP headers. However, having no caching will create it's own set of problems.

Is there a better solution?

Thanks, Don

+9  A: 

Why not just add a querystring "version" number and update the version each time?

foo.js -> foo.js?version=5

There still is a bit of work during the build to update the version numbers but filenames don't need to change.

Alison
agreed as common/good practice.
darma
Query strings are a bad idea if you care about caching because most proxy servers do not cache resources with a '?' in their URL. See http://code.google.com/speed/page-speed/docs/caching.html.
Paul Bevis
Paul - I think that the recommendations on that page are incomplete. Section 13.9 of RFC 2616 (the HTTP 1.1 spec) seems to imply that query strings can be cacheable if there's an explicit Expires header, so perhaps using a querystring is fine. But, this doesn't mean that all caches will honor this, so I'd probably stick with modifying the filenames myself.
Javid Jamae
+5  A: 

Renaming your resources is the way to go, although we use a build number and embed that in to the file name instead of an MD5 hash

foo.js -> foo.123.js

as it means that all your resources can be renamed in a deterministic fashion and resolved at runtime.

We then use custom controls to generate links to resources at on page load based upon the build number which is stored in an app setting.

Paul Bevis
A: 

I have also been thinking about this for a site I support where it would be a big job to change all references. I have two ideas:

1. Set distant cache expiry headers and apply the changes you suggest for the most commonly downloaded files. For other files set the headers so they expire after a very short time - eg. 10 minutes. Then if you have a 10 minute downtime when updating the application, caches will be refreshed by the time users go to the site. General site navigation should be improved as the files will only need downloading every 10 minutes not every click.

2. Each time a new version of the application is deployed to a different context that contains the version number. eg. www.site.com/app_2_6_0/ I'm not really sure about this as users bookmarks would be broken on each update.

Adam Butler
+7  A: 

The best solution seems to be to version filenames by appending the last-modified time.

You can do it this way: add a rewrite rule to your Apache configuration, like so:

RewriteRule ^(.+)\.(.+)\.(js|css|jpg|png|gif)$ $1.$3

This will redirect any "versioned" URL to the "normal" one. The idea is to keep your filenames the same, but to benefit from cache. The solution to append a parameter to the URL will not be optimal with some proxies that don't cache URLs with parameters.

Then, instead of writing:

<img src="image.png" />

Just call a PHP function:

<img src="<?php versionFile('image.png'); ?>" />

With versionFile() looking like this:

function versionFile($file){
    $path = pathinfo($file);
    $ver = '.'.filemtime($_SERVER['DOCUMENT_ROOT'].$file).'.';
    echo $path['dirname'].'/'.str_replace('.', $ver, $path['basename']);
}

And that's it! The browser will ask for image.123456789.png, Apache will redirect this to image.png, so you will benefit from cache in all cases and won't have any out-of-date issue, while not having to bother with filename versioning.

You can see a detailed explanation of this technique here: http://particletree.com/notebook/automatically-version-your-css-and-javascript-files/

PJP
This is by far the best solution here.
hopeseekr
Thanks ! :-) But the author from particletree deserves all the credit.
PJP
+2  A: 

We followed a similar pattern to PJP, using Rails and Nginx.

We wanted user avatar images to be browser cached, but on an avatar's change we needed the cache to be invalidated ASAP.

We added a method to the avatar model to append a timestamp to the file name:

return "/images/#{sourcedir}/#{user.login}-#{self.updated_at.to_s(:flat_string)}.png"

In all places in the code where avatars were used, we referenced this method rather than an URL. In the Nginx configuration, we added this rewrite:

rewrite "^/images/avatars/(.+)-[\d]{12}.png"    /images/avatars/$1.png;
rewrite "^/images/small-avatars/(.+)-[\d]{12}.png"      /images/small-avatars/$1.png;

This meant if a file changed, its URL in the HTML changed, so the user's browser made a new request for the file. When the request reached Nginx, it got rewritten to the simple name of the file.

pjmorse
+1  A: 

Most modern browsers check the if-modified-since header whenever a cacheable resource is in a HTTP request. However, not all browsers support the if-modified-since header.

There are three ways to "force" the browser to load a cached resource.

Option 1 Create a query string with a version#. src="script.js?ver=21". The downside is many proxy servers wont cache a resource with query strings. It also requires site-wide updating for changes.

Option 2 Create a naming system for your files src="script083010.js". However the downside to option 1 is that this as well requires site-wide updates whenever a file changes.

Option 3 Perhaps the most elegant solution, simply set up the caching headers: last-modified and expires in your server. The main downside to this is users may have to recache resources because they expired yet never changed. Additionally, the last-modified header does not work well when content is being served from multiple servers.

Here a few resources to check out: Yahoo Google AskApache.com

Moses
Lol read your answer after mine, almost exactly the same. +1 for hivemind.
+2  A: 

I would suggest using caching by ETags in this situation, see http://en.wikipedia.org/wiki/HTTP_ETag. You can then use the hash as the etag. A request will still be submitted for each resource, but the browser will only download items that have changed since last download.

Read up on your web server / platform docs on how to use etags properly, most decent platforms have built-in support.

Gintautas Miliauskas
A: 

If the file refrences are generated by the server, why not implement code such as...

<?php
$file = './whatever/example.js'
echo $file . "?q=" . sha1_file($file)
?>

sha1_file

This allows the query string to cause a refresh without you having to worry about it.

You can also tweak the server/file to send the cache headers you want with the file: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9

And a third option is to create a reference file that sends the 301 redirect on that request (anything_12340870870a8790ab.js) to the actual file anything.js. Requires server modification.

+1  A: 

This is really only an issue if your web server sets a far-future "Expires" header (setting something like ExpiresDefault "access plus 10 years" in your Apache config). Otherwise, a browser will make a conditional GET, based on the modified time and/or the Etag. You can verify what is happening on your site by using a web proxy or an extension like Firebug (on the Net panel). Your question doesn't mention how your web server is configured, and what headers it is sending with static files.

If you're not setting a far-future Expires header, there's nothing special you need to do. Your web server will usually handle conditional GETs for static files based on last modified time just fine. If you are setting a far-future Expires header then yes, you need to add some sort of version to the file name like your question and the other answers have mentioned already.

Bob
A: 

I believe that a combination of solutions works best:

  1. Setting cache expiry dates for each type of resource (image, page, etc) appropreatly for that resource, for example:

    • Your static "About", "Contact" etc pages probably arn't going to change more than a few time a year, so you could easily put a cache time of a month on these pages.
    • Images used in these pages could have eternal cache times, as you are more likey to replace an image then to change one.
    • Avatar images might have an expiry time of a day.
  2. Some resources need modified dates in their names. For example avatars, generated images, and the like.

  3. Some things should never be caches, new pages, user content etc. In these cases you should cache on the server, but never on the client side.

In the end you need to carfully consider each type of resource to determine what cache time to instruct the browser to use, and always be conservitive if you are unsure. You can increase the time later, but it's much more pain to uncache something.

thomasfedb
A: 

You might want to check out the approach taken by the grails "uiperformance" plugin, which you can find here. It does a lot of the things you mention, but automates them (set expiry time to a long time, then increments version numbers when files change).

So if you're using grails, you get this stuff for free. If you are not - maybe you can borrow the techniques employed.

Also - borrowed form the ui-performance page, - read the following 14 rules.

xcut