Correctly configuring HTTP caching can significantly improve page load performance, but misconfiguration can prevent users from seeing the latest content. You need to understand the mechanics to make good decisions.
Two Types of Caching
Browser makes a request
↓
Is there a cached version? → No → Request from server → Store cache → Return
↓ Yes
Is the strong cache valid? → Yes → Use cache directly (no server request) 200 from cache
↓ No
Send negotiation request to server → Unchanged → 304 Not Modified, use cache
→ Changed → 200, new content
Strong Cache
The browser checks if its cache is still valid — if it is, the cache is used directly with no request to the server.
Cache-Control (HTTP/1.1, takes priority)
# Server response header
Cache-Control: max-age=31536000 # cache for 1 year (in seconds)
Cache-Control: no-cache # skip strong cache, but allow negotiated cache
Cache-Control: no-store # no caching at all
Cache-Control: private # only the browser can cache, CDNs cannot
Cache-Control: public # both browser and CDN can cache
Expires (HTTP/1.0, lower priority)
Expires: Thu, 01 Jan 2019 00:00:00 GMT # expiry date (absolute time)
Drawback: depends on the client clock — if the client time is off, this breaks. Superseded by Cache-Control.
Negotiated Cache
The browser sends a cache identifier to the server, and the server decides whether the resource has changed.
Last-Modified / If-Modified-Since
# First request — server response
Last-Modified: Mon, 01 Jan 2018 10:00:00 GMT
# Subsequent requests — browser sends
If-Modified-Since: Mon, 01 Jan 2018 10:00:00 GMT
# Server decides: resource unchanged
HTTP/1.1 304 Not Modified
# Server decides: resource changed
HTTP/1.1 200 OK
Last-Modified: Mon, 15 Jan 2018 09:30:00 GMT
[new content]
Drawback: precision is only one second — multiple changes within one second are undetected.
ETag / If-None-Match (More Accurate)
# First request — server response
ETag: "abc123" # hash of the content
# Subsequent requests
If-None-Match: "abc123"
# Server compares ETag
HTTP/1.1 304 Not Modified # or 200 + new ETag
ETag is a content digest — if the content changes, the ETag changes. More accurate than Last-Modified.
Best Caching Strategy for Frontend Assets
HTML Files: No Cache or Negotiated Cache
location ~* \.html$ {
add_header Cache-Control "no-cache";
}
Reason: HTML is the entry point and must be updated promptly, so users always get the latest JS/CSS filenames.
Hashed JS/CSS: Maximize Strong Cache
location ~* \.(js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
Webpack builds files with content hashes in their names:
// webpack.config.js
output: {
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js'
}
This means app.a1b2c3d4.js keeps its hash (and cache) as long as the content doesn't change — and when it does, the filename changes (automatically invalidating the cache). You can safely set a 1-year strong cache.
Images and Fonts
location ~* \.(jpg|jpeg|png|gif|svg|woff2|ttf)$ {
add_header Cache-Control "public, max-age=2592000"; # 30 days
}
Correct Vue CLI / Webpack Configuration
// vue.config.js
module.exports = {
filenameHashing: true, // file name hashing is on by default
chainWebpack: (config) => {
// Ensure index.html is not cached
config.plugin("html").tap((args) => {
args[0].cache = false;
return args;
});
},
};
Verifying Cache Behavior
Chrome DevTools → Network panel:
200 (from memory cache)— strong cache (memory)200 (from disk cache)— strong cache (disk)304 Not Modified— negotiated cache hit200— cache miss, fetched from server
Size column showing 0 means the cache was used — no actual data was transferred.
Summary
- Don't use strong cache for HTML; use
no-cacheto ensure each visit is validated - Use content-hashed filenames for JS/CSS — safe to set maximum strong cache
- Negotiated cache with ETag is more accurate than
Last-Modified - Webpack's
contenthashis the key to correct cache invalidation