浏览器缓存技术指南

浏览器缓存(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 头部中的 ExpiresCache-Control 控制,由服务器响应时设置,在有效期内,浏览器可直接使用缓存,缓存命中,不再发起请求。

1
2
3
4
5
6
7
8
9
# 首次请求
GET /style.css HTTP/1.1

# 响应(缓存 1 小时)
HTTP/1.1 200 OK
Cache-Control: max-age=3600

# 1 小时内再次请求(浏览器直接使用缓存,不发请求)
GET /style.css HTTP/1.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
2
3
4
5
6
* max-age=xxx 过期时间,表示资源可以缓存多少秒(3600 即 1 小时),基于相对时间
* public 允许浏览器和代理服务器缓存该资源
* private 只允许浏览器缓存,代理服务器不能缓存
* no-cache 缓存,但不可直接使用,需通过协商缓存机制,经服务器验证后才能使用
* no-store 禁止缓存(不使用协商缓存,所有请求都会重新从服务器获取资源)
* immutable 表示资源不会改变,即使用户刷新页面也不会重新请求(Ctrl + R 强制刷新也不会重新请求,除非清除缓存),SPA 最佳拍档

其中,no-cache (必须验证) 和 max-age=0 (过期) 都表示直接使用协商缓存。

注意:为了向后兼容性,可同时设置 expiresCache-Control,比如 Nginx 配置中:

1
2
3
4
5
6
7
8
9
10
location /static/ {
# 旧版浏览器
expires 7d;
# 现代浏览器
add_header Cache-Control "public, max-age=604800";

# 注意:Nginx 中 expires -1 也会生成 Cache-Control: "no-cache" 头,导致下面配置出现两个 "no-cache" 头,移除 expires -1 即可
# expires -1;
# add_header Cache-Control "no-cache, must-revalidate";
}

协商缓存(Negotiated Cache)

协商缓存是指浏览器与服务器间通过协商来决定是否使用缓存。缓存过期(Cache-Control: max-age=n 到期)或不可直接使用(Cache-Control: no-cache)时,会通过协商缓存机制与服务器校验缓存是否仍然有效。如果资源没有改变,服务器会返回 304 Not Modified,浏览器继续使用本地缓存,否则返回 200 新的资源内容。

协商缓存利用 Last-ModifiedIf-Modified-SinceETagIf-None-Match 这两组 HTTP 头部控制。服务器响应设置 Cache-Control (如果直接使用协商缓存则应设为 max-age=0no-cache,否则等待强缓存过期)、Last-Modified, ETag,浏览器再次请求携带缓存校验信息 If-Modified-SinceIf-None-Match,服务器校验返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 首次请求
GET /index.html HTTP/1.1

# 响应(Cache-Control: max-age=0 表示必须进行协商缓存)
HTTP/1.1 200 OK
Cache-Control: max-age=0
Last-Modified: Wed, 20 Mar 2025 12:00:00 GMT
ETag: "abc123"

# 再次请求(携带缓存校验信息)
GET /index.html HTTP/1.1
If-Modified-Since: Wed, 20 Mar 2025 12:00:00 GMT
If-None-Match: "abc123"

# 响应
HTTP/1.1 304 Not Modified
  • 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 依赖服务器时间,如果服务器时间被修改(比如时钟回拨),可能会导致缓存错乱。

为了更精准地判断资源是否变化,可以使用 ETagETag 更精确,不受时间精度影响,能识别内容相同但修改时间不同的情况。

  • 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
2
3
Cache-Control: no-cache
ETag: "abc123"
Last-Modified: Mon, 20 Mar 2025 10:00:00 GMT

对于 JS、CSS、图片等更新不频繁的静态资源文件,采用 Cache-Control: max-age 强缓存机制。但应配合文件指纹或内容哈希使用,否则强缓存机制会导致缓存更新问题。

1
Cache-Control: max-age=31536000, immutable

现代前端开发中,文件名普遍使用内容哈希,这类资源,可放心采用强缓存机制。

CSR 项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 默认采用协商缓存
location / {
try_files $uri $uri/ /index.html;

add_header Cache-Control "no-cache, must-revalidate"; # "public, max-age=0, must-revalidate" 效果一样
# etag on; # Nginx 中 ETag 默认启用,无需设置,Last-Modified 也会自动设置,特殊需求下可手动设置,如 add_header Last-Modified $date_gmt 设置为动态时间,永远返回 200
}

# 带哈希的静态文件采用强缓存
# location /assets/ 基于目录匹配比基于扩展名白名单匹配会更简单
location ~* \.[a-f0-9]{8,}\.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
}

SSR 项目:

1
2
3
4
5
location /_nuxt/ {
root /white-page/public;
expires max;
add_header Cache-Control "public, max-age=31536000, immutable";
}
  • 缓存更新问题

在强缓存机制下,甚至没有设置 ETag 的协商缓存机制下,可能出现更新资源无法生效问题,浏览器会继续加载旧资源。

可使用以下方案解决:

1
2
* 使用带 Cache-Control: no-cache + Last-Modified + ETag 的协商缓存机制,不走强缓存
* 使用强缓存机制,但资源带文件指纹(如 app.js?v=abc1234)或内容哈希(如 app.abc123.js)