This site is being remade and I won't be updating this for a while. Expect a lot of broken links, design and functionality. If all goes well, things should be stable by the end of 2022. Thank you for understanding.

HomePostsLinks

Improving Netlify's Default HTTP Cache-Control Headers for Immutable Assets

Published

I decided to use the Recursive font on my site once again and when it went live, they kept popping in between subsequent reloads. Looking into it, it turns out that this was due to how Netlify caches things.

What does Netlify do?

In essence, they include this header:

cache-control: public, max-age=0, must-revalidate

As they put it, the header says please cache this content, and then do not trust your cache. This is useful for assets that change frequently as the browser will always check if an asset has changed before fetching it by verifying the ETag header.

Seems ok. What’s the problem?

The issue I had was that certain assets on this site (like CSS, JS, and font files) have cache busted URLs where the URL either has a hash or version number.

<link rel=stylesheet href=/fonts.1baec3efa7adb966c2c5c92e8258e75a350c5151d2679de09650f4536b185be8.css>
<link rel=stylesheet href=/main.ba62e1ad92ea86749a3f67394100f75f0044a087c2101678a42e21ddffdaaa3f.css>

Note the hashes in the hrefs. That corresponds to the file’s hash. If the underlying file changes, the hash would as well.

There’s no need to constantly revalidate such an asset as it will never change. If the asset did change, it would have a new URL that would be cached on its own. As fast as Netlify’s CDN is and however much they want to include an ETag, revalidation is still pointless overhead for fingerprinted URLs.

How To Fix This

To fix this, I used the immutable cache-control header which tells the browser that the asset will not be updated as long as it’s fresh. Note that the maximum caching age is a year which is about 31,536,000 seconds).

Cache-Control: public, immutable, max-age=31536000

To do this, you need to provide custom headers. I used an _headers file which looks like this:

/*.woff2
    Cache-Control: public, max-age=31536000, immutable
/*.css
    Cache-Control: public, max-age=31536000, immutable
/*.js
    Cache-Control: public, max-age=31536000, immutable

This works on my site since CSS and JS files will always be fingerprinted1, and the .woff2 font files contain the version in the URL so if the font is updated, so will the URL.

Conclusion

After doing this, the site loaded a bit faster and the pop-in didn’t go away. It’s an improvement, though I need to quantify it at some point. That’s a benefit of going beyond the defaults. I know I put Netlify here, but I’m sure you can apply this to any host that let’s you change HTTP headers

This is all possible thanks to Hugo which comes with a good asset pipeline. If Hugo didn’t have this, or I decided not to use cache busted URLs, sticking to must-revalidate would be fine.

Interestingly, a few days after I solved this, I found a guide by Simon Hearne which goes into caching headers best practices which covers other scenarios.

The next performance optimization I want to do after this is preloading assets. The last time I tried it nothing changed but I hope it’ll be different this time.


  1. Even if I include a library through NPM, I’d still cache bust it. ↩︎