← Recipes

阅读中文版本

Version Static Assets for Long-Lived Caching

Use content-hashed filenames with immutable caching, keep HTML revalidated, and verify a deploy never serves mismatched assets.

~25 mincachingfrontenddeployment

Task

Let browsers cache JavaScript and CSS for a year while every deploy still reaches users immediately.

When to use this

A deploy replaces app.css under the same URL. Some browsers keep the old file for a week, and the new HTML renders without styles.

Prerequisites

  • A bundler that emits content-hashed filenames
  • Control over Cache-Control headers at the CDN or origin
  • An HTML entry point that references the hashed files

Steps

1. Configure the bundler to put a content hash in every asset name.

export default {
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash][extname]',
      },
    },
  },
};

Expected output: Build output contains names such as app.9f2c1a.js that change whenever the content changes.

2. Serve hashed assets as immutable and HTML with no-cache.

/assets/*
  Cache-Control: public, max-age=31536000, immutable

/index.html
  Cache-Control: no-cache

Expected output: Returning visitors revalidate only the HTML and load assets from cache instantly.

3. Verify the headers actually served by the edge.

curl -sI https://app.example.com/assets/app.9f2c1a.js | grep -i cache-control && curl -sI https://app.example.com/ | grep -i cache-control

Expected output: The asset reports max-age=31536000 with immutable; the HTML reports no-cache.

4. Check that the HTML references only files present in the deploy.

grep -o '/assets/[^"]*' dist/index.html | while read -r f; do
  test -f "dist$f" || echo "missing $f"
done

Expected output: No missing file is printed; a broken reference fails the deploy instead of shipping a broken page.

Troubleshooting

Problem

Old HTML references an asset that no longer exists

Solution

Deploy HTML and assets together and keep the previous asset set for at least one TTL, or make the HTML part of the same atomic deploy.

Problem

Cache-Control is not what the configuration says

Solution

A CDN rule or reverse proxy may rewrite headers. Check the response at the edge, not only at the origin.

Limitations

  • Immutable caching makes rollback harder if old hashed assets are deleted; retain previous builds.
  • A service worker adds another cache layer that must be versioned separately.

Hashes turn caching into a solved problem

A URL that never changes cannot be cached forever, because the content behind it does change. Content hashing fixes that by making the URL a function of the bytes: when the file changes, the name changes, and the old URL can stay immutable forever. The HTML becomes the only mutable document, so it must be revalidated on every visit. This split is what lets a deploy be instant for users while still allowing a one-year cache for assets.

Set two different policies

Assets under a hashed path get max-age=31536000, immutable; HTML, the service worker, and any manifest get no-cache so the browser revalidates before using them. A common mistake is applying the immutable policy at a path prefix that also serves non-hashed files, which then never update. Another is leaving the HTML cached for hours, so users keep requesting deleted asset names until the cache expires.

/assets/*
  Cache-Control: public, max-age=31536000, immutable

/index.html
  Cache-Control: no-cache

Deploy atomically, keep history

Upload assets before HTML, and keep the previous asset set available for at least one caching window, because a browser may still hold the old HTML when the deploy finishes. Check that every reference in the new HTML exists in the deploy, and fail the build when it does not. When rolling back, redeploy the previous HTML rather than assuming the old assets are still there. If a service worker is involved, version its cache name in the same commit, since it can serve stale content independently of the CDN.