CloudFront Functions for an Efficient Cache Policy
Using CloudFront functions to apply some clever and sensible Cache-Control headers to a fully static Next.js app.
This site itself is a static Next.js app. It's a bunch a static assets, living in AWS S3 and served through CloudFront. CloudFront does a great job at caching resources but when it came time for a Lighthouse audit, this one — Serve static assets with an efficient cache policy — was failing for me.
Why? All my assets were missing a
Given the serverless nature of this site, there were two spots where I could apply cache-control headers — S3 and CloudFront Functions.
While you can set
Cache-Control headers via item metadata in S3, you probably don't want to do this.
Initially, I set the
Cache-Control header to
as a baseline value that I intended to overwrite for specific routes, later down the response journey
(specifically in a CloudFront function).
However, this resulted in a minor bug. The
max-age=0 value returned by S3, is also used by CloudFront itself to
determine how long to cache a certain resource, and in this case, it effectively told CloudFront, "always refresh me".
I realized this after deploying some changes, and then cURL'ing my website URL multiple times.
I was expecting a
Hit from cloudfront value for the
curl --head https://thekevinwang.com/
...but instead I received
RefreshHit from cloudfront every time. Documentation around the difference between "RefreshHit"
& "Hit" pretty much doesn't exist, but this forum post was what
I referenced to conclude that the previous behavior was undesirable.
CloudFront functions were where I ended up setting my header values.
CloudFront Functions were introduced around May 2021 and serve 4 documented use cases:
- Cache-key manipulations and normalization
- URL rewrites and redirects
- HTTP header manipulation 👈👈👈
- Access authorization
The particular kind of function I used was the Viewer Response — it's triggered right before a response is returned to a requesting client.
I wrote two simple CloudFront Functions to modify the cache-control header to
for routes that I wanted to cache, and
public,max-age=0,must-revalidate for routes that I didn't want to be cached.
I used these in conjunction with CloudFront Behaviors to apply the functions to the proper routes.
I didn't want the
html documents to be cached, or else an end-users' browser might be loading an older,
cached version of the site when a newer deployment is actually available.
I did want everything else —
json, etc. — to be cached because the
html files have references
to all the static assets that they need. Next.js is smart about generating unique hashed filenames on each new build
of your project.
Going back to the original failing Lighthouse audit, this is the result after applying the necessary cache-control headers.
Here's a glimpse of what my CDK code for provisioning two CloudFront Functions, and adding multiple CloudFront behaviors looks like.
This is a strange, easy to resolve error.