Major performance gains are to be had from browser caching CSS. You ensure your server is set up to send headers that tell the browser to hang onto the CSS file for a given amount of time. It’s a best-practice that many if not most sites are doing already.
Hand-in-hand with browser caching is cache busting. Say the browser has the CSS file cached for one year (not uncommon). Then you want to change the CSS. You need a strategy for breaking the cache and forcing the browser to download a new copy of the CSS.
Here are some ways.
The CSS has to be cached for this to matter…
Just to make sure, here’s what some healthy looking headers look like for a cached CSS file:

We’re looking for that Cache-Control
and Expires
header. I’m not a server config expert. I’d probably look at the H5BP server configs. But here’s some kinda classic Apache/HTAccess ways to get that going:
<FilesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)(\.gz)?$">
Header set Expires "Thu, 15 Apr 2020 20:00:00 GMT"
</FilesMatch>
<IfModule mod_expires.c>
ExpiresActive on
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
</IfModule>
Query Strings
Most browsers these days will see a URL with a different query string as a different file and download a fresh copy. Most CDN’s even support and recommend this.
<link rel="stylesheet" href="style.css?v=3.4.1">
Make small change? Change it to:
<link rel="stylesheet" href="style.css?v=3.4.2">
You could potentially make it easier on yourself by setting a server side variable to use in multiple places. Thus changing it would break cache on lots of files at once.
<?php $cssVersion = "3.4.2"; ?>
<link rel="stylesheet" href="global.css?v=<?php echo $cssVersion; ?>">
Perhaps you could even use Semantic Versioning. You could also define a constant.
Changing File Name
Query strings didn’t always work. Some browsers didn’t see a differnt query string as a different file. And some software (I’ve heard: Squid) wouldn’t cache files with query string. Steve Souders told us not to.
A similar concept was to change the file name itself. Like this in the HTML:
<link rel="stylesheet" href="style.232124.css">
You would handle this programmatically, not literally change the file name in your project. Since that file doesn’t actually exist on the server, you’ll need to perform some trickery to route it to the right file. Jeremy Keith covered his technique for this fairly recently.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+).(\d+).(js|css)$ $1.$3 [L]
That tells the server to ignore those numbers in JavaScript and CSS file names, but the browser will still interpret it as a new file whenever I update that number.
He uses Twig, so the templates he uses are ultimately like:
{% set cssupdate = '20150310' %}
<link rel="stylesheet" href="/css/main.{{ cssupdate }}.css">
I’m sure you can imagine a version of that in any backend language (like ASP). Level up by making a build tool or deployment script update the variable itself.
Basing Cache Busting “Number” on File Updated Date
While searching around about this cache busting stuff, you’ll see a lot of advice recommending you use the server to check when the file was last updated to create the cache busting “number” (number, meaning, whatever thing you change to bust cache).
function autoversion($url) {
$path = pathinfo($url);
$ver = '.'.filemtime($_SERVER['DOCUMENT_ROOT'].$url).'.';
return $path['dirname'].'/'.str_replace('.', $ver, $path['basename']);
}
<link href="<?php autoversion('/path/to/theme.css'); ?>" rel="stylesheet">
I can’t speak well to this. It seems to me that asking your server to dig up this information on every pageview would be pretty intensive and dangerous in production. In the past I’ve done things like “I’ll just have PHP output the dimensions of the image in data attributes!” only to find it grinds the server to halt. Anyway, beware.
ETags
ETags kinda seem like a good idea, because the whole point of them is information to check if the browser already has a copy of that file.
But most advice out there says: “turn off your ETags headers”. Yahoo says:
The problem with ETags is that they typically are constructed using attributes that make them unique to a specific server hosting a site. ETags won’t match when a browser gets the original component from one server and later tries to validate that component on a different server, a situation that is all too common on Web sites that use a cluster of servers to handle requests.
Another issue is that they just aren’t as effective as actual caching. In order to check an ETag, a network request still needs to be made. It’s not just the downloading of files that affect performance, it’s all the network negotiation and latency stuff too.
Again, not an expert here, but here’s what’s generally recommended to turn them off in Apache land:
<IfModule mod_headers.c>
Header unset ETag
</IfModule>
FileETag None
Framework Does It For Us
Rails Asset Pipeline
I have a little experience with the Rails Asset Pipeline and Sprockets. It’s kind of a dream system if you ask me. I link up stylesheets in templates:
<%= stylesheet_link_tag "about/about" %>
And it produces HTML like:
<link href="http://assets.codepen.io/assets/about/about-7ca9d3db0013f3ea9ba05b9dcda5ede0.css" media="screen" rel="stylesheet" type="text/css" />
That cache busting number only changes when the file changes, so you only break cache on the files that need broken. Plus it has methods for images and JavaScript as well.
WordPress
If you use a page caching tool in WordPress, like W3 Total Cache or something, you probably have to be less afraid of that filemtime
business being too server intensive.
Gilbert Pellegrom posted a WordPress-specific technique using it:
wp_register_style( 'screen', get_template_directory_uri().'/style.css', array(), filemtime( get_template_directory().'/style.css' ) );
wp_enqueue_style( 'screen' );
// Example Output: /style.css?ver=1384432580
The WordPress plugin Busted! does this same thing behind the scenes. Just does it kinda automatically to everything.
CodeKit
CodeKit doesn’t have a built in method for changing file names, but it does have a way to execute Shell scripts under circumstances you set up.

Michael Russell has a blog post about how you can inject timestamps into file themselves, which I’m sure you could modify to change the filenames instead.
Build Tools
All the popular task runner / build tool thingies have plugins to help change file names. Sufian Rhazi has a post on doing it in raw Node.js as well.
Grunt
Gulp
Broccoli
Within Preprocessors
When linking up assets within other assets (e.g. an image that you link to from within a LESS file, for example) you could put the preprocessor to work. Ben Nadel has a post on doing just that.
Async CSS
With Critical CSS becoming more of a thing, deferred loading of CSS is becoming more of a thing. There are some other reasons to defer loading of CSS as well (perhaps print CSS, or priming cache).
If you’re loading CSS with loadCSS (or perhaps injecting a link tag), you’ll need to update the file name it requests in the JavaScript itself. Different than changing the file name, but not that different.
So
Anything I missed? What’s your cache busting strategy?
I work at a large university (12000+ students) and we use the ‘last updated’ approach in a .net environment. We haven’t had any issues with servers ‘grinding to a halt’. We have the automated query strings on 4 different files. It works like a charm and integrates great with our team workflow. With .net we also have the luxury of caching different parts of pages or caching controls. I can’t speak on other languages, but for us, it is working great.
Django can also use a far future Expire headers by configuring ManifestStaticFileStorage. It works similarly to how rails does it. Link to your files like this:
And the output will be like this:
The rewrite rule on “Changing File Name” section is wrong. Dots should be escaped, eg:
RewriteRule ^(.+)\.(d+)\.(js|css)$ $1.$3 [L]
, otherwise they would match any character, not just dots.I don’t think semantic versioning for CSS is a very practical idea. Keeping track of what CSS is a patch and what’s an improvement and what’s a breaking change would be pretty difficult and totally pointless when you can just force every browser to download the latest version.
Agreed!
I use a PHP include that makes both minification and cache busting:
If the website is in production, of course the minified CSS is not writable anymore, and the script is reduced to:
The htaccess is the same as Stephano told:
Of course, depending of the CMS, no need to do all of these :)
Checking an mtime is a very quick operation, certainly compared to asking the server to open and read and parse image files to get their pixel dimensions.
Personally, I’d recommend optimising developer time and get the server to check the mtime. You can always optimise for performance later if any performance isn’t good enough (this won’t happen).
This is how W3 Total Cache is doing it. Rails looks to be calculating an MD5, which is a way more intensive operation (but still no likely to tax a webserver).
^ I think this comment is spot on.
I’d be surprised if even getting image sizes would slow down a server meaningfully — assuming we’re talking about local images, and not ones located on another website/server (which would need to be downloaded first!).
File access isn’t as big a deal as you might think. Bear in mind that a typical framework (e.g. Rails) requires a large number of files to be accessed for every request — that’s just for booting up the framework. In PHP, you can check how many by adding this code to your page:
<?php var_dump( count(get_included_files()) ) ?>
I just checked this on my site, which uses Laravel 4. I have 200 files loaded on every request.
Oh, and because numbers are good…
See this comment on the PHP get_image_size() page, where someone ran a few tests. Apparently it took 0.15 seconds to retrieve the image dimensions of 10,000 files.
Goddammit, incompetent people like me could use an edit button. ;)
http://php.net/manual/en/function.getimagesize.php#94178
I also prefer the md5 way, because the file access is cached by the filesystem-layer and you can also add it into twig. e.g.:
gulp-rev is probably the most common gulp plugin for this.
It produces a filename based on the contents of the file as well as a json file with a list of all the names that have been changed and what they were changed to.
I’m a bit confused regarding the recommendation to remove ETags. This article, by Ilya Grigorik, recommends to add them, but H5BP seems to removes them
I guess if you’re only using one server, you should add ETags, otherwise remove them?
The comment on that H5BP github page gives you a clue. Etags are pointless if you’re setting far-future expires headers.
An Etag can let the server tell the browser whether the file has been modified since the last download. If you are setting expires headers of (say) a year, or 10 years, then the browser will never ask the question — because long before that date comes around, the item will have been flushed out the browser cache anyway, and will need to be downloaded afresh.
Stopping the browser from even asking the question is better, because you have less “chat” between browser and server over HTTP.
Of course, the “downside” (if you can call it that) of setting far future expiry is that you have to handle cache-busting.
Great article as always, very informative and helpful. Just noticed one little spelling error in the second sentence under “Changing File Name” missing an “e” from different.
We use
gulp-rev
. I didn’t see it mentioned here, but it’s similar to what Rails, Laravel Elixir, etc. do.Basically, you use
gulp-rev
to rename your asset files based on their contents. Note that this works for CSS files, JS files, image files, anything. Once your files are renamed, you read the manifest file to map the original file name to the renamed file, and serve that instead.This is extremely quick because the server is serving a regular file – no rewrites, no PHP logic, nothing. And if you’re worried about reading the manifest on every page load, you can easily cache it in Redis or memcached for quick lookups.
The other benefit here is that you don’t have to keep any sort of manual version counter. All you need to do is run a gulp task and you’re done. And if a given file doesn’t change, its file name doesn’t change, so the cache isn’t busted for that particular file.
gulp-rev is probably the most common gulp plugin for this.
These Strategies which i use for my site
A few notes.
Squid and query strings
You mentioned that this was something that Steve Souders discovered. It had to do with the default configuration shipping with Squid at the time. That default was changed in Squid 2.7, which released 7 years ago.
I asked Steve if he still considered this an issue. Response: “Not sure. Hard to test. I’m not aware of known issues.”
While 7 years doesn’t guarantee you won’t run into Squid proxies still running with that default config, I’d put query string usage for versioning in the ‘fairly safe’ category.
Etags
This is another issue where the default configuration was a problem, this time in Apache. It used
FileETag INode MTime Size
. The main problem was using inode, which was certainly going to be different from server to server. This was fixed in 2.4, when the default switched toFileETag MTime Size
.Since this is something the web server controls, fixing it is easy in most cases.
You are correct that this shouldn’t be used in place of caching, it should be ( properly ) used along side caching.
Having tried all methods over the years that Chris mentions here, nothing works quite as well as very long cache times with assets “fingerprinted” with their checksum. This is the approach that Rails/Sprockets and Ember take out of the box.
It’s the most consistent and only forces a redownload of the assets when they change.
This does contribute a bit to “asset churn” if you are making lots of changes constantly. In most situations, this is an acceptable tradeoff. HTTP/2 will solve this and will drastically change how we package and serve assets.
I got tired of manually updating filenames in EE templates, so I made a small plugin to do the cache-busting for me: https://github.com/vfalconi/cache-override
Example:
produces:
really like to see more of this