Serve static assets with an efficient cache policy

For optimal performance, it's considered good practice to put a long cache lifetime on your static assets (CSS, JS, images, etc). This means that these files will be cached in the visitors' browser after their first visit, improving page load times on return visits.

There is one downside though: when you update any of these static assets, returning visitors will not see them unless they manually refresh their cache. Any file that was encountered before, is retrieved from cache (if it's not expired). So we need to tell their browser that a new version of the file is available, so it will be downloaded again.

The solution is simple: change the filename every time the asset is updated. A common trick is to use a query string (assets/css/site.css?v=123) but apparently that is not 100% reliable. Much better to change the filename itself. But pfff, rather cumbersome to build that into your build process...

Use Nginx to handle dynamic file paths

Server config to the rescue! You can tell your web server to look for a specific pattern in the filename (containing the version number) and ignore that bit, so it always serves the original file path. In the browser however, the filename contains a version number or timestamp, which forces the file to be downloaded again after an update.

Perfect! Here's how to set it up with Nginx:

server {
  ...
  # Add cache header to static assets
  location ~* '\.(?:jpg|jpeg|gif|png|ico|gz|svg|svgz|mp4|ogg|ogv|webm|htc|css|js|ttf|ttc|otf|eot|woff)$' {
    expires 1y;
    add_header "Cache-Control" "public";

    # Remove version number from URI
    location ~* '(.+)\.(?:[^.]+)\.(jpg|jpeg|gif|png|ico|gz|svg|svgz|mp4|ogg|ogv|webm|htc|css|js|ttf|ttc|otf|eot|woff)$' {
      try_files $uri $1.$2;
    }
  }
}

The second location directive sniffs out the version number and returns the original path.

Test first with a shorter expiry setting! If you're feeling confident after that, you can even set it to 'max'.

Activate cache busting

Romanesco has a cache busting setting built-in. It's under Configuration > Performance. When enabled, it looks for a version number in system settings and integrates it into the filename.

There are 3 different areas that can be busted independently:

  • CSS (through the system setting romanesco.assets_version_css)
  • JS (through romanesco.assets_version_js)
  • Favicons (through romanesco.favicon_version)

Dealing with updated images

In the given example, images are also cached, but they don't receive a cache busting string from Romanesco. This means that if you upload a new version of an image, you can't use the same filename.

Using a different filename is of course an easy fix, but if you don't want to do that (and if you're thumbnailing your images with pThumb), you can set the phpthumbof.check_mod_time to true. The "updated on" timestamp is now checked, and if it's different, a new hash will be generated for the thumbnail. This time, it's MODX to the rescue!

Other things to consider

  • Updating the Configuration > Presentation settings will bump the CSS version number.
  • In theory, critical CSS version number should be bumped every time it's regenerated. But in practice, it seems very unlikely that an updated critical causes flashes of incorrectly styled content, because the main assets will be loaded from cache very fast.