no-cache does not mean "do not store": the browser keeps the copy but asks first, and a matching ETag earns a body-less 304. A file named by its content hash (Vite 25,978 writes index-DMFBxjQs.css) never changes under that name, so it may stay a year; a new build is a new URL, which is all cache busting is. immutable also skips revalidation on reload in Firefox 49 555 + and Safari 11 10 +; Chrome 1 ignores it.
# /var/www/example/.htaccess: revalidate pages, keep fingerprinted assets for a year
# Needs: mod_headers; AllowOverride FileInfo
FileETag MTime Size
<FilesMatch "\.(html|php|json)$">
Header set Cache-Control "no-cache"
</FilesMatch>
<FilesMatch "-[A-Za-z0-9_-]{8}\.(css|js|mjs|woff2|png|jpg|svg|webp|avif)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
# Let a compressed copy's ETag ("...-gzip", "...-br") still earn a 304
RequestHeader edit* If-None-Match "-(gzip|br)\"" "\""With Accept-Encoding: gzip: the stylesheet, the page, then the page with the ETag it just sent:
200 OK|"383f9-65c21a708438d-gzip"|public, max-age=31536000, immutable 200 OK|"103-65c21b6311193-gzip"|no-cache 304 Not Modified|"103-65c21b6311193"|no-cache
The ETag is size and mtime in hex (line 3, the default since 2.4 dropped the inode). Compression adds a suffix, and Apache 129 then fails to match its own tag: without line 11 the third request got 200 and the whole page. DeflateAlterETag NoChange fixes it, but only server-wide. Behind a load balancer, deploy identical mtimes or use FileETag Digest, a SHA-1 of the content that survived a touch.