When I develop ClojureScript projects, I almost always use Figwheel. It’s a great tool, but sometimes my app ended up using stale files. This led to some very confusing debugging sessions. It only happened some of the time, and was always fixed after a hard refresh. I thought about just disabling the browser cache, but I didn’t like ignoring the issue. After seeing colleagues struggle with stale caching too, I decided to figure out what was going on, and fix it once and for all.
Cache-Control rules everything around me
The first thing to do was to add a
Cache-Control: no-cache header to all static file responses. Despite the name,
no-cache tells the browser it can cache files, but must always validate them with the server before using them. If the browser’s cached version is up-to-date, a compliant HTTP server should return a 304 Not Modified response, otherwise it serves the new file.
If you don’t provide a caching header to an HTTP response, the browser can choose its own caching behaviour. The browser’s caching heuristics are much more aggressive than you want in development, and lead to the weird caching behaviour I was seeing.
I thought this had fixed the issue, but occasionally I would still notice stale files were being used. After looking closely at the compiled output files, I made a surprising discovery.
ClojureScript copies file modification times
goog.base), gets a modification time that matches the time it was compiled.
Neither of these dates are particularly useful to use as a
Last-Modified date header for caching purposes.
- A ClojureScript file that uses macros from other Clojure files will get the modification date of the consuming ClojureScript file, even if the macro files have changed more recently and caused a recompilation. This was leading to the second round of caching issues that I saw.
To avoid these issues, I recommend removing the
Last-Modified header from the response when in development.
To knock both problems on the head once and for all (hopefully), I added a CRC32 checksum based ETag for static file responses. I packaged this up in a library ring-etag-middleware so that other projects could also use it.
Serve 304 Not Modified responses
At this point the browser will check with the server for every ClojureScript file, on every pageload. However, this causes all of the files to be downloaded each time, even if they haven’t changed. The last step is to add ring’s
ring.middleware.not-modified/wrap-not-modified middleware. This returns a “304 Not Modified” response if the ETag provided in the
If-None-Match request header matches the
ETag header in the response.
As best as I can tell, this has completely solved all of the odd caching issues that I was seeing, while still keeping the app snappy to load by reusing as much of the cache as possible. If you are serving ClojureScript files in development and not using Figwheel, I recommend you follow these three steps:
- Set a
- Add an ETag to your static file responses
- Remove the
- Wrap your responses in
ring.middleware.not-modified/wrap-not-modifiedor the equivalent in your Clojure web framework.