Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with:

Cache-Busting CSS Images With Less CSS

By Ben Nadel on
Tags: HTML / CSS

This is a really minor post, but one of the features that I love about Less CSS and variable interpolation is the ability to cache-bust the images you consume in your CSS. Since the browser caches your CSS files and your images as separate items, it's easy to remember to cache-bust your CSS file, but forget to cache-bust your various background images and sprites. We can fix this by adding cache-busting query-parameters to our image Urls.

To demonstrate, take a look at this Less CSS code. I'm defining a global cache-busting variable that can be used to cache-bust images whenever the Less is compiled. This value can then be overridden for a more static alternative in your various modules:

  • // By default we can cache-bust any image source by using the current time at compile
  • // time (milliseconds since epoch). This value can then be overridden in cases where
  • // you may want a longer-lived source.
  • // --
  • // CAUTION: Im not really recommending that you cache-bust every single time you
  • // compile your Less CSS; mostly, I just wanted an exuste to put JavaScript in here.
  • @cache-version: `( new Date() ).getTime()` ;
  •  
  • body {
  • background-image: url( "../img/background.png?version=@{cache-version}" ) ;
  • }
  •  
  • div.m-header {
  • // In this case, we want the cache-busting of this module to be static since we
  • // know that it will not change very often. To do this, we can simply override
  • // the global value, which will create a block-local version.
  • @cache-version: 20140611.3 ;
  •  
  • h1 {
  • background-image: url( "../img/background.png?version=@{cache-version}" ) ;
  • }
  • }

Since variables, in Less CSS, are locally-scoped to each block, the variable in the div.m-header module does not affect the global one. When we compile the above Less CSS, we get the following CSS output:

  • body {
  • background-image: url("../img/background.png?version=1402484407427");
  • }
  • div.m-header h1 {
  • background-image: url("../img/background.png?version=20140611.3");
  • }

As you can see, the background image Url on the body now includes the current timestamp, which means that it will be cache-busted every time the Less CSS is compiled. The div.m-header module, on the other hand, has a static value that has to be manually changed by the developer whenever the image is known to have changed.

Cache-busting never makes sense at either extreme - you probably don't want to cache-bust on every compilation; but, you also want to prevent users from seeing stale, outdated images. If nothing else, picking a static cache-busting value for each module will lay the foundation for easy cache-busting in the future. Plus, simply seeing the cache-busting variable is a great reminder that cache-busting is a thing that needs to be done (in general).

Tweet This Deep thoughts by @BenNadel - Cache-Busting CSS Images With Less CSS Thanks my man — you rock the party that rocks the body!



Reader Comments

Your post has prompted me to revisit the subject of cache busting, and it turns out that appending a query string is not really the best approach. It's better to change the filename. Well, probably anyway. If you are interested, I have left a short answer on Stackoverflow on the topic: http://stackoverflow.com/a/24166106/508355

Cheers, Michael

Reply to this Comment

@Michael,

Interesting. I have not had too much experience with proxies. I do work with CDN networks for caching and (at least the one I use with the out-of-the-box settings) will only cache based on the file name. Meaning, if you make two requests to through the cache with the same filename and different query-strings, I *believe* it will serve up the same file.

If it doesn't have the file cached, it will pass the query-string through to the origin server and the origin server can then use the various query-strings for whatever purpose.

I think Amazon S3 works similarly. The query-string is only for validation and meta-data; I don't think it really affects the file (unless you use object version stuff).

But, please take what I just said with a grain-of-salt as I have no experience with proxies and very little experience with CDNs. But, it sounds like the topic is more complex than one might first imagine.

Thanks for the mental simulation!

Reply to this Comment

@Ben,

what you say seems to match up with Souder's observations; ie, if one _really_ needs to fetch a new version of a file, and get past all caches until they are refreshed, using a revved filename is the only way which is guaranteed to work.

Thanks for your insights!

Reply to this Comment

Nice one Ben.

Another way that I do this (in case your interested :p) is I have in my css something like:

url( "../img/background.png?version=@hash" )

I then use Gulp to build my CSS and after it's built, I have hash plugin that I wrote that will read all of my files (html/css/js) and match a certain pattern (looking for @hash), if it finds it, it generates the md5 sum of that file and replaces @hash with sum.

We then server this to the browser, and let the browser figure out if the asset is different or not. So if background.png changes, the sum will be different and the browser will automatically reload it.

This has allowed us to only reload the necessary files (only the ones that changed) on any release.

Reply to this Comment

@Jose,

That's a really cool approach. I understand what you're saying, though I only know Gulp (and the like) from what I have heard. I have not played around with it too much. I did listen to a Gulp show on JavaScript Jabber. It should pretty cool - all Node.js streams. It's definitely on my list of things to explore!

Reply to this Comment

Using a timestamp isn't a very safe way of forcing the browser to refresh in my opinion. It could also cause collisions (so it won't get refreshed) in the browser cache if the requested URI doesn't vary enough.

I normally use fingerprints in the requested resource path and URL rewriting on the server side.

Google has an explanation here: https://developers.google.com/speed/docs/best-practices/caching

You can then use one fingerprint for all static resources, or several for images, css, js etc.

This method also works transparently with CDNs too.

@Ben: Been a regular reader over the years, first time poster. Just wanted to say a quick thanks for your quality posts. Cheers

Reply to this Comment

@Duncan Hello, other first-time poster ;)

That's what I do, too - and I use the last-modified date of a file, pulled from the file system, as the fingerprint. You can't have two different resources with the same timestamp in the same fs location, can you? So how would that lead to collisions?

Reply to this Comment

@Michael,

No that cant happen. I was talking about client side caching browser behaviour. I have had the problem on at least one version of Firefox. I would say it comes down to how each browser handles caching.

An example using Bens timestamp method:

1. Browser requests and caches "/img/background.png?version=1402484407427"

2. Browser requests "/img/background.png?version=1402484574271", but since the URL is very similar it hashes to the same value in the browser cache. And nothing gets refreshed.

This may not really be anything to worry about anymore, and could be considered a bug in the browser. Its just something I had to battle with once, and I only got around it by using large fingerprints early in the path name. EG: /4a4d993ed7bd7d467b27af52d2aaa800/img/background.png

Cheers

Reply to this Comment

I see. We've been posting about the same thing then, more or less ;)

It's a bit odd that your fingerprint had to show up early in the url-path, and that it had to be large. Paths like /img/1402484407427/background.png or /img/background-1402484407427.png should work just as well. But then again, weird bugs do exist...

Cheers, Michael

Reply to this Comment

@Michael,

Yeah, it was about five years ago. Which in ages in internet time :) So I might not remember all the details correctly and it could very well be not relevant any more.

Cheers

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.