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.
Kevin Wang
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 Cache-Control
header.
Passing the audit
Given the serverless nature of this site, there were two spots where I could apply cache-control headers — S3 and CloudFront Functions.
S3 Cache-Control Headers
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 public,max-age=0,must-revalidate
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 x-cache
header...
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
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 public,max-age=31536000,immutable
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.
Cache static assets; Don't cache HTML
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 — js
, css
, 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.
Lighthouse Success
Going back to the original failing Lighthouse audit, this is the result after applying the necessary cache-control headers.
AWS CDK TypeScript Snippet
Here's a glimpse of what my CDK code for provisioning two CloudFront Functions, and adding multiple CloudFront behaviors looks like.
Gotchas
No let
or const
in CloudFront Functions
This is a strange, easy to resolve error.