静态资源被设成 no-cache:Cache-Control 与 ETag 的搭配笔记
HTTP headers 本质是一对 key: value,用冒号隔开,为每个 request / response 提供额外信息。做网站性能时,缓存相关的几个头是关键中的关键。
Cache-Control:先分清指令语义
Cache-Control 统一控制缓存行为,覆盖旧的 Expires / Pragma。
Cache-Control: public, max-age=31536000, immutable
public:任何 cache(CDN、browser)都能存private:只给 end-user browser cache,不给 CDN,用于登录后的个人化内容no-store:完全不要 cache,用于敏感数据no-cache:可以存,但每次用前要先跟 server 验证max-age=N:缓存 N 秒immutable:告诉 browser 内容绝对不会变,省下 revalidate 的 requeststale-while-revalidate=N:过期后 N 秒内仍可用旧内容,后台去更新
Cache-Control 与 Expires 同时设置时以 Cache-Control 优先(RFC 9111),Expires 只是兼容 HTTP/1.0 老代理。
三种典型取值:
# 带 hash 的静态资源 app.a3f8b2.js
Cache-Control: public, max-age=31536000, immutable
# HTML
Cache-Control: no-cache
# API JSON
Cache-Control: private, max-age=60
max-age 内不碰 server,过期带 If-None-Match 去 revalidate
ETag 是 server 对 response body 算的指纹(hash),放在 response 里。If-None-Match 是 client 下次 request 把上次拿到的 ETag 带回去,server 比对如果一样,回 304 Not Modified,没有 body,省带宽。
# 第一次 GET /api/users
curl -i https://example.com/api/users
# → 200 OK
# ETag: "abc123"
# { ... }
# 第二次 GET /api/users
curl -i https://example.com/api/users -H 'If-None-Match: "abc123"'
# → 304 Not Modified(body 为空)
If-Match:PUT 的 lost update 与 412
多人同时编辑一份 wiki 时,PUT 会互相覆盖。带上 If-Match 做前置校验:
curl -i -X PUT https://example.com/wiki/page-1 \
-H 'If-Match: "abc123"'
如果现在的 ETag 还是 "abc123" → 200 OK 更新成功;如果已经被别人改过 → 412 Precondition Failed。
Last-Modified 是 ETag 的时间版本,两者都在时 ETag 优先
Server 回 Last-Modified: Sun, 05 Sep 2021 01:40:14 GMT,client 下次带 If-Modified-Since 同一时间,没改就 304。它的精度比 ETag 低;同时有 ETag 和 Last-Modified 时,ETag 优先。
Vary 过于宽松,CDN 命中率会掉下去
Vary 让缓存按请求头区分缓存条目。CDN 命中率低的时候,先检查 Vary 字段是不是过于宽松——带上 User-Agent 会导致几乎不命中。
Vary: User-Agent
地雷:Cache-Control 与 ETag 没搭好
Cache-Control: max-age=3600 和 ETag 同时用时:1 小时内直接用 cache,不会碰 server;1 小时后带 If-None-Match 去 revalidate,回 304 就继续用。
常见错误是给静态资源设 Cache-Control: no-cache,每次都 revalidate,性能很差,但很多人误以为这样比较「安全」。
其他头速查
| 类别 | 常见 headers |
|---|---|
| General | Date / Host / User-Agent |
| Content Negotiation | Accept / Accept-Language / Accept-Encoding |
| Payload | Content-Type / Content-Length / Content-Encoding / Content-Disposition |
| Connection | Connection / Keep-Alive |
| Security | HSTS / CSP / X-Content-Type-Options / X-Frame-Options / Referrer-Policy / Permissions-Policy |
| CORS | Access-Control-Allow-Origin 等 |
| Cookie & Auth | Set-Cookie 的 Secure / HttpOnly / SameSite=Strict、Lax、None,以及 Authorization、WWW-Authenticate |
同区域的其他坑:API 没设 Content-Type;Cookie 没设 SameSite,或设成 None 却忘了 Secure;CORS 用 Allow-Origin: * 又配 cookie;直接信任 X-Forwarded-For 做 IP 判断。
上线前缓存头检查清单
- 带 hash 的静态资源是
public, max-age=31536000, immutable,不是no-cache - HTML 是
no-cache,没有套用长 max-age - 登录后的个人化接口用
private,敏感数据用no-store - 响应带 ETag 或 Last-Modified;两者都有时确认 ETag 优先
- PUT 类写操作有 If-Match 校验,冲突时返回 412
- Vary 没有带上 User-Agent 这类每次请求都不同的头
- Cache-Control 与 Expires 同时存在时,取值不互相矛盾
- Content-Type、SameSite / Secure、CORS 与 cookie 的搭配一并过一遍