Improving Netlify's Default HTTP Cache-Control Headers for Immutable Assets
Tags: webperf, netlify, http
Published: 2022-02-14 13:07:25

---
title: Improving Netlify's Default HTTP Cache-Control Headers for Immutable Assets
published: true
date: 2022-02-14 13:06:33 UTC
tags: [webperf,netlify, http]
canonical_url: https://codelab.farai.xyz/notes/improving-http-headers/
---

I decided to use the [Recursive font](https://recursive.design/) 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](https://www.netlify.com/blog/2017/02/23/better-living-through-caching/).

## What does Netlify do?

In essence, they include this header:

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

As they put it, the header says <q>please cache this content, and then do not trust your cache</q>. 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.

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

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

To do this, you need to [provide custom headers](https://docs.netlify.com/routing/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 fingerprinted<sup id="fnref:1"><a href="#fn:1" role="doc-noteref">1</a></sup>, 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](https://gohugo.io/hugo-pipes/introduction/). 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](https://simonhearne.com/2022/caching-header-best-practices/#the-solution) 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.

<section role="doc-endnotes">
<hr>
<ol>
<li id="fn:1" role="doc-endnote">
<p>Even if I include a library through NPM, I’d still cache bust it. <a href="#fnref:1" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</section>