浏览器缓存技术指南

浏览器缓存(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 控制。

  • 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
location /static/ {
# 旧版浏览器
expires 7d;
# 现代浏览器
add_header Cache-Control "public, max-age=604800";
}

协商缓存(Negotiated Cache)

缓存过期或 Cache-Control 被设置为 max-age=0no-cache,这时会采用协商缓存机制与服务器验证缓存是否仍然有效。如果资源没有改变,服务器会返回 304 Not Modified,浏览器继续使用本地缓存。

注:有缓存下,会优先使用强缓机制,缓存过期才使用协商缓存机制。

协商缓存利用 Last-ModifiedIf-Modified-SinceETagIf-None-Match 这两组 HTTP 头部控制。

  • Last-Modified 和 If-Modified-Since
1
Last-Modified: Mon, 20 Mar 2025 10:00:00 GMT

服务器返回资源时,会带上 Last-Modified 头部,表示资源的最后修改时间。浏览器在下次请求时,会在请求头中带上 If-Modified-Since,其值是上一次请求时返回的 Last-Modified 值,询问服务器资源是否更新。

1
If-Modified-Since: Mon, 20 Mar 2025 10:00:00 GMT

服务器再次收到资源请求时,根据传过来 If-Modified-Since 和资源在服务器上的最后修改时间比较日期大小,判断资源是否有变化,如果资源没有变更,服务器返回 304 Not Modified,浏览器继续使用缓存,否则,服务器返回新的资源。Last-Modified 头部会更新为新生成的返回。

注意:如果资源内容没有变,但 Last-Modified 发生变化,仍然会触发重新下载。

  • ETag 和 If-None-Match

Last-Modified 只能精确到秒级,在高频操作中,如果资源在 1 秒内被多次修改,Last-Modified 无法区分这些变化,另外 Last-Modified 依赖服务器时间,如果服务器时间被修改(比如时钟回拨),可能会导致缓存错乱。

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

1
ETag: "abc123"

ETag 是资源的唯一标识符(通常是文件的哈希值)。浏览器请求时,会带上 If-None-Match 头部,值是上一次请求时返回的 ETag 的值。

1
If-None-Match: "abc123"

服务器再次收到资源请求时,根据传过来 If-None-Match 和新生成的 ETag 比较,如果相同,返回 304 Not Modified,否则返回新的资源。ETag 头部会更新为新生成的返回。

浏览器缓存策略的选择

对于 HTML 文件这样更新频率高的文件,采用 Cache-Control: no-cache + Last-Modified + ETag 协商缓存机制。

1
2
Cache-Control: no-cache
ETag: "abc123"

对于 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
15
16
# 默认采用协商缓存
location / {
try_files $uri $uri/ /index.html;

expires -1;
add_header Cache-Control "no-cache, must-revalidate"; # "public, max-age=0, must-revalidate" 效果一样
add_header Last-Modified $date_gmt;
etag on; # 启用 ETag
}

# 带哈希的静态文件采用强缓存
# 基于目录匹配比基于扩展名白名单匹配会更简单
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)