This post goes through how to reduce the amount of static assets served through Netlify by leveraging a CDN (Content Delivery Network) like Cloudflare. This will reduce your Netlify bandwidth usage for cacheable assets.
It will also mean that you’re following industry-accepted best practices around setting a time for which your assets should be cached by receiving browsers.
In principle, it won’t significantly improve the speed at which your site loads since Netlify’s built-in CDN does a good job of that anyways. It will mean that you’re leveraging your CDN’s capacity to deliver assets quickly as opposed to Netlify’s.
What is Netlify’s default caching behaviour?
The default Netlify cache-control header, are "max-age=0, must-revalidate, public"
which means all content should be cached but also re-validated. The rationale is that “This favors you as a content creator — you can change any of your content in an instant.” - Better Living Through Caching.
This means that web performance tests will give you lower marks for not having a reasonable caching length for static assets.
Netlify suggests not to use a CDN in front of it (see Why You Don’t Need Cloudflare with Netlify). The core points of this article is that Netlify provides the following which a CDN also tend to help with:
- SSL: Netlify creates certificates so that your site is always served over HTTPS.
- CDN: Netlify’s CDN is very clever when used along its managed DNS service, with smart load balancing/routing and DDoS prevention baked in.
- DNS: Setting up (custom) domains with Netlify’s DNS is fast and simple.
These 3 services are services also provided by Cloudflare.
Setting caching headers on Netlify
The Netlify documentation for Custom headers doesn’t have a caching example. It also suggests there are 2 ways to add custom headers: through a _headers
file and as part of a [[headers]]
section of your netlify.toml
.
Here’s a sample netlify.toml
setup for codewithhugo.com, ignore the [build]
section or see the The netlify.toml File article in Netlify’s knowledge base:
[build]
command = "./scripts/build.sh"
publish = "public"
functions = "lambda"
[[headers]]
for = "/img/*"
[headers.values]
Cache-Control = "public, s-max-age=604800"
[[headers]]
for = "/*.css"
[headers.values]
Cache-Control = "public, s-max-age=604800"
[[headers]]
for = "/*.js"
[headers.values]
Cache-Control = "public, s-max-age=604800"
The equivalent _headers
file:
/img/*
Cache-Control: public, s-max-age=604800
/*.css
Cache-Control: public, s-max-age=604800
/*.js
Cache-Control: public, s-max-age=604800
The Cache-Control
we’re setting is public, s-max-age=604800
. We’re using s-max-age
which according to the MDN Cache-Control article is:
Takes precedence over
max-age
or theExpires
header, but it only applies to shared caches (e.g., proxies) and is ignored by a private cache.
This means we’re using custom headers on Netlify to tell Cloudflare (our CDN) how long to cache assets. We’ll then control how long browsers cache using CDN settings.
604800 seconds translates to 7 days, so during normal operation Cloudflare will cache images, CSS and JavaScript files for 7 days.
Cache rules breakdown
From Cloudflare statistics I know that images + CSS account for more than 60% of my traffic.
Image caching
Serving images out of Netlify doesn’t seem too useful since they don’t change often. They’re also not actually part of the core codewithhugo.com offering, they’re “nice-to-have” as opposed to the “must-have” text + code content. That’s where the first rule comes in:
[[headers]]
for = "/img/*"
[headers.values]
Cache-Control = "public, s-max-age=604800"
or in _headers
format:
/img/*
Cache-Control = "public, s-max-age=604800"
CSS caching
The CSS I generate through Hugo has a cache-busting hash baked in. So old versions and new versions will be fetched and cached separately. It’s therefore safe to aggressively cache across the CDN and in browsers.
In netlify.toml
headers format:
[[headers]]
for = "/*.css"
[headers.values]
Cache-Control = "public, s-max-age=604800"
In _headers
format:
/*.css
Cache-Control: public, s-max-age=604800
JavaScript caching
I don’t leverage a lot of JavaScript and whatever I do use usually ends up as a couple of script tags on a page. The pre-built JavaScript that I consume is safe to cache for long periods.
In netlify.toml
headers format:
[[headers]]
for = "/*.js"
[headers.values]
Cache-Control = "public, s-max-age=604800"
In _headers
format:
/*.js
Cache-Control: public, s-max-age=604800
Caching Results
My cached requests percentage has gone from 5% to over 65%. Since Cloudflare is serving most of my heaviest content (images), it’s resulted in an 80% Cloudflare → Netlify bandwidth usage reduction.
Caching assets with @Cloudflare
Left ~5%, default Netlify headers ()
Right 50%+, with Cache-Control: public, s-max-age=604800
ie. Allow a CDN in front of this site to cache assets 1 week
— Hugo Di Francesco (@hugo__df) March 20, 2019