浏览器缓存机制

强缓存与协商缓存的工作原理和使用场景

问题

浏览器缓存分为强缓存和协商缓存,它们分别是如何工作的?Cache-Control、Expires、ETag、Last-Modified 各有什么作用?

解答

缓存流程

请求资源

检查强缓存(Cache-Control/Expires)

命中 → 直接使用缓存(200 from cache)
未命中 ↓
发送请求,携带协商缓存标识(If-None-Match/If-Modified-Since)

服务器验证

未修改 → 304 Not Modified,使用缓存
已修改 → 200 OK,返回新资源

强缓存

不向服务器发送请求,直接从缓存读取资源。

# Expires(HTTP/1.0)- 绝对时间,受客户端时间影响
Expires: Wed, 21 Oct 2025 07:28:00 GMT

# Cache-Control(HTTP/1.1)- 相对时间,优先级更高
Cache-Control: max-age=31536000

Cache-Control 常用指令:

# 缓存 1 年
Cache-Control: max-age=31536000

# 禁止缓存
Cache-Control: no-store

# 每次都要验证(走协商缓存)
Cache-Control: no-cache

# 只能被浏览器缓存,不能被 CDN 缓存
Cache-Control: private

# 可以被 CDN 等中间代理缓存
Cache-Control: public

协商缓存

向服务器发送请求,由服务器判断是否使用缓存。

Last-Modified / If-Modified-Since:

# 首次响应,服务器返回资源最后修改时间
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

# 再次请求,浏览器携带该时间
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

ETag / If-None-Match:

# 首次响应,服务器返回资源唯一标识
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 再次请求,浏览器携带该标识
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

Node.js 实现示例

const http = require('http');
const fs = require('fs');
const crypto = require('crypto');

http.createServer((req, res) => {
  const filePath = './static' + req.url;
  const stat = fs.statSync(filePath);
  const content = fs.readFileSync(filePath);
  
  // 生成 ETag
  const etag = crypto.createHash('md5').update(content).digest('hex');
  
  // 获取最后修改时间
  const lastModified = stat.mtime.toUTCString();
  
  // 检查协商缓存
  if (req.headers['if-none-match'] === etag ||
      req.headers['if-modified-since'] === lastModified) {
    res.writeHead(304);
    res.end();
    return;
  }
  
  // 设置缓存头
  res.setHeader('Cache-Control', 'max-age=3600'); // 强缓存 1 小时
  res.setHeader('ETag', etag);
  res.setHeader('Last-Modified', lastModified);
  
  res.end(content);
}).listen(3000);

实际应用策略

# Nginx 配置示例

# HTML - 不缓存或短时间缓存
location ~* \.html$ {
    add_header Cache-Control "no-cache";
}

# 带 hash 的静态资源 - 长期缓存
location ~* \.(js|css|png|jpg|gif|ico)$ {
    add_header Cache-Control "max-age=31536000, immutable";
}

# API 接口 - 不缓存
location /api/ {
    add_header Cache-Control "no-store";
}

关键点

  • 强缓存优先级:Cache-Control > Expires,前者用相对时间更可靠
  • 协商缓存优先级:ETag > Last-Modified,前者基于内容 hash 更精确
  • Last-Modified 缺陷:精度只到秒级,文件内容不变但修改时间变了也会失效
  • 缓存策略:HTML 用 no-cache 保证更新,带 hash 的静态资源用长期缓存
  • 304 vs 200:304 只返回响应头,节省带宽;200 from cache 完全不发请求