浏览器缓存(Browser Cache)是指浏览器会将访问过的资源(如 HTML、CSS、JS、图片等)存储在本地,以便在后续请求时可以直接从缓存中获取,而不是重新向服务器请求。在 Web 开发中,浏览器缓存是一项至关重要的优化技术,它能够减少网络请求,提升页面加载速度,提高用户体验,同时降低服务器负载,节省服务器带宽。
缓存机制主要通过 HTTP 头部控制,包括强缓存(Strong Cache)和协商缓存(Negotiated Cache)。强缓存会直接读取本地缓存,协商缓存则是与服务器协商读取。强缓存和协商缓存被同时设置时,会优先使用强缓,缓存过期才使用协商缓存。
缓存机制需要服务器支持,强缓存需要设置响应缓存头部,协商缓存不但需要设置响应缓存头部,还需要实现校验逻辑。
%%{
init: {
'theme': 'base',
'themeVariables': {
'primaryBorderColor': '#7C0000',
'lineColor': '#fff',
'tertiaryColor': '#fff',
'fontSize': '12px',
'max-width': '10000px'
}
}
}%%
graph TD;
A[浏览器请求资源] --> B{是否存在缓存}
B -- 否
no-store --> C[向服务器请求资源]
C --> D{决策返回
200 or 304?}
D -- 200 --> E[设置缓存信息
Last-Modified, ETag]
E --> F[使用资源]
D -- 304 --> G[更新缓存信息
Last-Modified, ETag]
G --> F
B -- 是 --> H{是否可直接使用}
H -- 否
no-cache --> I[获取验证信息
If-Modified-Since / If-None-Match]
I --> C
H -- 是 --> K{是否过期
expires/max-age/s-maxage}
K -- 是 --> I
K -- 否 --> L[从缓存中获取资源]
L --> F
(缓存控制流程图)
强缓存(Strong Cache)
强缓存是指浏览器直接从本地缓存中读取资源,状态码返回 200,无需向服务器发起请求。
强缓存利用 HTTP 头部中的 Expires 和 Cache-Control 控制,由服务器响应时设置,在有效期内,浏览器可直接使用缓存,缓存命中,不再发起请求。
1 | # 首次请求 |
- Expires
Expires 指定资源的过期时间,在此时间之前,浏览器直接从缓存读取资源,无需请求服务器。
1 | Expires: Fri, 22 Mar 2025 12:00:00 GMT |
Expires 在 HTTP/1.0 规范中定义,由于 Expires 是服务器设置的绝对时间,可能会因客户端与服务器时间不同步而失效,因此在 HTTP/1.1 中被 Cache-Control 取代。
- Cache-Control
Cache-Control 是 HTTP/1.1 中引入,提供了更精细的缓存控制。
1 | Cache-Control: max-age=3600, public |
Cache-Control 一些常设置值:
1 | * max-age=xxx 过期时间,表示资源可以缓存多少秒(3600 即 1 小时),基于相对时间 |
其中,no-cache (必须验证) 和 max-age=0 (过期) 都表示直接使用协商缓存。
注意:为了向后兼容性,可同时设置 expires 和 Cache-Control,比如 Nginx 配置中:
1 | location /static/ { |
协商缓存(Negotiated Cache)
协商缓存是指浏览器与服务器间通过协商来决定是否使用缓存。缓存过期(Cache-Control: max-age=n 到期)或不可直接使用(Cache-Control: no-cache)时,会通过协商缓存机制与服务器校验缓存是否仍然有效。如果资源没有改变,服务器会返回 304 Not Modified,浏览器继续使用本地缓存,否则返回 200 新的资源内容。
协商缓存利用 Last-Modified,If-Modified-Since 和 ETag、If-None-Match 这两组 HTTP 头部控制。服务器响应设置 Cache-Control (如果直接使用协商缓存则应设为 max-age=0 或 no-cache,否则等待强缓存过期)、Last-Modified, ETag,浏览器再次请求携带缓存校验信息 If-Modified-Since 、If-None-Match,服务器校验返回。
1 | # 首次请求 |
- Last-Modified 和 If-Modified-Since
Last-Modified 头部,表示资源的最后修改时间,服务器返回资源时,携带 Last-Modified 头部,浏览器在下次请求时,会在请求头中携带 If-Modified-Since,其值是上一次请求时返回的 Last-Modified 值,询问服务器资源是否更新。
1 | Last-Modified: Mon, 20 Mar 2025 10:00:00 GMT |
1 | If-Modified-Since: Mon, 20 Mar 2025 10:00:00 GMT |
服务器再次收到资源请求时,根据传过来 If-Modified-Since 和资源在服务器上的最后修改时间比较日期大小,判断资源是否有变化,如果资源没有变更,服务器返回 304 Not Modified,浏览器继续使用缓存,否则,服务器返回 200 新的资源,且 Last-Modified 头部会更新为新生成的返回。
Last-Modified 只能精确到秒级,在高频操作中,如果资源在 1 秒内被多次修改,Last-Modified 无法区分这些变化,另外 Last-Modified 依赖服务器时间,如果服务器时间被修改(比如时钟回拨),可能会导致缓存错乱。
为了更精准地判断资源是否变化,可以使用 ETag,ETag 更精确,不受时间精度影响,能识别内容相同但修改时间不同的情况。
- ETag 和 If-None-Match
ETag (Entity Tag) 是资源的唯一标识符,通常是文件的哈希值。浏览器请求时会携带 If-None-Match 头部,值是上一次请求时返回的 ETag 的值。
1 | ETag: "abc123" |
1 | If-None-Match: "abc123" |
服务器再次收到资源请求时,根据传过来 If-None-Match 和新生成的 ETag 比较,如果相同,返回 304 Not Modified,否则返回新的资源,且 ETag 头部会更新为新生成的返回。
浏览器默认缓存机制
即使服务器没有响应 Cache-Control 缓存头,如果满足某些条件,浏览器仍会使用默认缓存机制对资源进行缓存。现代浏览器(比如 Chrome)默认的缓存机制是启发式缓存(Heuristic Caching),这依赖于 Last-Modified (如果响应中连 Last-Modified 也没有,浏览器则会完全放弃缓存,视为 no-store),计算规则为:
1 | 缓存时间 = (响应 Date - Last-Modified) * 10% |
比如,Last-Modified 是 10 天前,浏览器则会缓存 1 天。如果不希望浏览器使用默认缓存机制自行推测缓存时间,应需明确设置 Cache-Control。
服务器实现
在 NodeJS 静态资源服务器中缓存的实现,详情见 npm 包 @telei/explorer。
浏览器缓存机制的选择
对于 HTML 文件这样更新频率高的文件,采用 Cache-Control: no-cache + Last-Modified + ETag 协商缓存机制。
1 | Cache-Control: no-cache |
对于 JS、CSS、图片等更新不频繁的静态资源文件,采用 Cache-Control: max-age 强缓存机制。但应配合文件指纹或内容哈希使用,否则强缓存机制会导致缓存更新问题。
1 | Cache-Control: max-age=31536000, immutable |
现代前端开发中,文件名普遍使用内容哈希,这类资源,可放心采用强缓存机制。
CSR 项目:
1 | # 默认采用协商缓存 |
SSR 项目:
1 | location /_nuxt/ { |
- 缓存更新问题
在强缓存机制下,甚至没有设置 ETag 的协商缓存机制下,可能出现更新资源无法生效问题,浏览器会继续加载旧资源。
可使用以下方案解决:
1 | * 使用带 Cache-Control: no-cache + Last-Modified + ETag 的协商缓存机制,不走强缓存 |