← Recipes

阅读中文版本

Set HTTP Cache Headers for Static Assets and HTML

Give fingerprinted assets a long immutable cache while forcing browsers and CDNs to revalidate HTML, preventing stale deployments without sacrificing repeat-visit performance.

~15 minperformancehttpcaching

Task

Configure response headers so hashed assets are cached for a year and HTML entry points always revalidate.

When to use this

A deployment replaces JavaScript bundles, but browsers keep serving old HTML that references deleted assets, or every asset is downloaded again on each visit.

Prerequisites

  • A web server or framework that can set response headers
  • Build output with content-hashed asset filenames
  • Access to the CDN configuration if one is used

Steps

1. Confirm that asset filenames contain a content hash. Only then is a long immutable cache safe.

ls dist/assets | head

Expected output: Filenames such as app.3f9c2a1b.js with a content hash.

2. Serve hashed assets with a one-year immutable cache header.

app.use('/assets', express.static('dist/assets', {
  immutable: true,
  maxAge: '1y',
}));

Expected output: Response includes Cache-Control: public, max-age=31536000, immutable.

3. Serve HTML with revalidation so a new deployment is visible immediately. Avoid long max-age on HTML.

app.get('*', (_req, res) => {
  res.set('Cache-Control', 'no-cache');
  res.sendFile(path.resolve('dist/index.html'));
});

Expected output: HTML responses include Cache-Control: no-cache and revalidate with ETag/Last-Modified.

4. Verify headers with curl and confirm a deploy changes the HTML while hashed assets remain cached.

curl -I https://example.com/assets/app.3f9c2a1b.js && curl -I https://example.com/

Expected output: Asset has max-age=31536000, immutable; HTML has no-cache.

Troubleshooting

Problem

Old HTML references deleted assets after deploy

Solution

HTML is being cached too long. Switch entry HTML to no-cache or a very short max-age and ensure the CDN honors origin headers.

Problem

Assets change without a hash change

Solution

Disable long caching until the build emits content-hashed filenames.

Limitations

  • CDN behavior may override origin headers; verify the edge response, not only the local server.
  • Immutable caching cannot be revoked for a URL; use a new hash instead.

The two-class policy

Treat HTML and hashed assets differently. HTML is the entry point and must update on deploy; hashed assets are content-addressed and can be cached forever because a change produces a new URL.

app.use('/assets', express.static('dist/assets', {
  immutable: true,
  maxAge: '1y',
}));

app.get('*', (_req, res) => {
  res.set('Cache-Control', 'no-cache');
  res.sendFile(path.resolve('dist/index.html'));
});

Verify

  1. curl -I on an asset shows Cache-Control: public, max-age=31536000, immutable.
  2. curl -I on / shows Cache-Control: no-cache and an ETag or Last-Modified.
  3. After a deploy, a normal reload fetches the new HTML and references new hashed assets.
  4. The CDN edge response matches the origin policy.

Pitfalls

no-cache does not mean “do not store”; it means “revalidate before use.” no-store prevents caching entirely and is rarely needed for HTML. If your CDN strips or overrides origin headers, configure the cache policy at the edge as well. Never give HTML a one-year immutable cache unless you can change the URL on every deploy.