skip to content
Aaron Becker

Astro + Nginx: Caching Headers for Static Assets

/ 4 min read

In my previous post I showcased my self-updating Dockerfile for an nginx + brotli container image. This time I’ll show how to set caching headers for static assets in that same container, which will further reduce loading time if users visit multiple pages on the same site.

what are static assets?

Vite bundles client-side JavaScript, CSS, images, and data files into optimized and hashed files, as I mentioned in this post. These files aren’t endpoints that a client browser directly requests, and they don’t change unless you rebuild your site—so it’s safe to cache them indefinitely without checking whether they’ve changed.

Nginx will assign each HTML file a unique ‘ETag’ value, which is a hash of the file’s contents. This allows a browser to send a conditional request, which will re-use existing content if the file hasn’t changed, but only after checking with the server. This differs from the type of caching applied in the next section, where the browser doesn’t even need to check whether a file has changed.

what are caching headers?

The Cache-Control header controls whether, and for how long, a browser should cache a response. Static assets from a build tool that applies cache-busting can be cached for a long time—most examples set the Cache-Control header to max-age=31536000 (one year). The immutable directive can be added to let the browser know that the asset never changes, so it doesn’t need to check for updates (“revalidate”).

why set caching headers?

Caching headers aren’t strictly necessary to achieve a perfect Lighthouse performance score, but the Lighthouse performance audit will recommend that you configure them with the tagline “Serve static assets with an efficient cache policy”:

Lighthouse serve static assets with an efficient cache policy

The performance impact of caching headers will be greatest for users who visit multiple pages on the same site. Due to Nginx’s support for conditional requests unchanged static assets will not need to be re-downloaded even without configuring caching headers, but it’s still a good idea to add them because it removes the need to check with the server1.

how to set caching headers

First, to ensure that static assets are consistently built into the same output location, explicitly set the build.assets path in your astro.config.ts:

astro.config.ts
export default defineConfig({
build: {
assets: "_astro",
},
// ...rest of config...
});

Second, configure Nginx to set the caching headers. The following example shows how to set them for static assets served from the _astro directory, building on the Dockerfile from my previous post:

Dockerfile
# ...rest of Dockerfile...
# 2. Server configuration with error pages and caching headers
RUN printf 'server {\n\
listen 80;\n\
server_name _;\n\
root /usr/share/nginx/html;\n\
index index.html;\n\
\n\
# Cache Astro static assets for 1 year\n\
location /_astro/ {\n\
add_header Cache-Control "public, max-age=31536000, immutable";\n\
}\n\
\n\
error_page 404 /404.html;\n\
location = /404.html {\n\
internal;\n\
}\n\
}\n' > /etc/nginx/conf.d/default.conf
# ...rest of Dockerfile...

conclusion

There you have it: another micro-optimization for your Astro + Nginx + Docker stack. At the very least this will silence a Lighthouse audit warning, and it might even make your site load a little faster for users who visit multiple pages. Caching headers can be much more impactful than they’re likely to be in this particular case; they’re worth understanding if you have control over your server configuration.

Footnotes

  1. With HTTP/2 multiplexing these revalidation requests can be done in parallel, so the overhead of checking with the server should be minimal if you’re already making a round-trip to get a new HTML page. Nginx is just awesome like that. If you have a large number of static assets, however, you might notice some revalidation overhead that these caching headers can eliminate.