Enable CDN (Cloudflare) caching for your Netlify site

Hugo Di Francesco - Apr 14 '19 - - Dev Community

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"
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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 the Expires 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"
Enter fullscreen mode Exit fullscreen mode

or in _headers format:

/img/*
  Cache-Control = "public, s-max-age=604800"

Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

In _headers format:

/*.css
  Cache-Control: public, s-max-age=604800

Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

In _headers format:

/*.js
  Cache-Control: public, s-max-age=604800

Enter fullscreen mode Exit fullscreen mode

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

pic.twitter.com/QijWByIbOF

— Hugo Di Francesco (@hugo__df) March 20, 2019

unsplash-logo
Zbynek Burival

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .