Skip to main content
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Matthew Eash
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Matthew Eash ( @mujimu )

Cache-Busting CSS Images With Less CSS

By on
Tags:

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).

Want to use code from this post? Check out the license.

Reader Comments

4 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

15,674 Comments

@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!

4 Comments

@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!

2 Comments

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.

15,674 Comments

@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!

3 Comments

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

4 Comments

@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?

3 Comments

@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

4 Comments

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

3 Comments

@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

1 Comments

I stumbled upon this article, when researching cache busting in LESS.

I did discover another method for doing this, which is the `url-args` option in the LESS config. This can be used to add a cache busting mechanism, whether it be a commit hash, timestamp or anything else.

When using Grunt to compile the LESS, it could be done like this:
```
less: {
development: {
options: {
urlArgs: new Date().getTime()
},
files: {
'dist/main.css': 'src/less/main.less'
}
}
}
```

1 Comments

Hi Ben,

Something further I found when researching this (having found this post!):

Less.js and the requirejs less task have a modifyVars option, so you can override/reset less variables.

So, you could set @logo: "logo.png"; in your default less stylesheet.

Then, at build-time, do e.g.:

options: {
modifyVars: {
'logo': '"logo-1.1.png"'
}
}

I started writing some code to manage this from a manifest file, but then discovered hashly [1] and the attendant grunt-hashly [2] plugin, which copies all or part of your folder structure, creating hashed versions of the filenames and (optionally) processing your CSS for you, replacing the original filename with the hashed version.

What's really nice is that it will choke when it fails to find a local image that's referenced in the CSS, keeping things neat.

And because it uses MD5 hashes, it only changes the references to things that have actually changed.

[1] https://github.com/labaneilers/hashly
[2] https://github.com/labaneilers/grunt-hashly

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel