浏览器缓存彻底讲透:强制缓存 + 协商缓存 + Nginx 配置 + 前端实战

6 阅读8分钟

更多文章可以关注 【 小敏碎讲前端 】

 一 简单了解缓存位置

  • 内存缓存:快速但容量小
  • 磁盘缓存:持久化但比较慢
  • 可编程缓存:可编程缓存存储 + 业务逻辑代码,让你能够根据实际需求"写程序"来决定缓存的每一个细节,而不是被动接受缓存框架的固定行为

二 协商缓存 与 强制缓存

2.1 协商缓存

每次请求都询问服务器自选是否有更新,返回304或新资源(简单来说就是浏览器拿缓存的"标签"去问服务器"还能用吗",服务器回答"能用"(304)或"不能用了,拿新的吧"(200))

image.png

2.1.1 如何实现协商缓存

 (1) ETag(优先使用)

服务器给资源打的"版本号"或"指纹":

# 首次响应,服务器返回 ETag
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Type: text/html

[资源内容]

# 再次请求,浏览器带上 If-None-Match
GET /index.html HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 资源未变,服务器返回 304
HTTP/1.1 304 Not Modified

ETag 的计算方式

  • 文件内容的哈希值(如 SHA1)
  • 文件大小 + 修改时间的组合
  • 服务器自定义的版本号
(2) Last-Modified(备用)

服务器告诉浏览器资源的最后修改时间:

# 首次响应
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT

# 再次请求,浏览器带上 If-Modified-Since
GET /index.html HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT

# 未修改,返回 304
HTTP/1.1 304 Not Modified

2.1.2 其他知识点

(1) 为什么有 Last-Modified 还要 ETag?
  • Last-Modified 精度只到秒,毫秒级变化检测不到
  • 文件修改时间可被伪造
  • 文件内容未变但修改时间变了(如重新上传)
(2) 304 请求消耗大吗?
  • 比完整请求小很多(无响应体)
  • 但仍需建立连接、查数据库验证
  • 适合验证频率不高的场景

2.2 强制缓存

在缓存有效期内,直接使用本地缓存,不发送请求到服务器(是一种不需要询问服务器,浏览器直接根据本地缓存的过期时间来决定是否使用缓存的机制。)

2.2.1 如何实现强制缓存

(1) Cache-Control
# 服务器响应头
Cache-Control: max-age=3600

常用指令

指令含义
max-age=3600缓存3600秒(1小时)
public任何地方都可以缓存(CDN、代理)
private只能浏览器缓存,不能给CDN
no-cache不是不缓存,而是每次都要协商
no-store完全不缓存
immutable永不变,适合带hash的静态资源
# 组合使用
Cache-Control: public, max-age=31536000, immutable

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

# 必须协商(实际是协商缓存)
Cache-Control: no-cache
(2) Expires(HTTP/1.0,已过时)
Expires: Wed, 21 Oct 2025 07:28:00 GMT

问题:依赖客户端时间,用户改系统时间就会出问题。现在基本被 Cache-Control 替代。

如果同时存在,Cache-Control 优先级更高。

2.3 强缓存 vs 协商缓存

对比项强缓存协商缓存
是否请求服务器否(过期前)
网络消耗01次请求(无Body)
速度极快(毫秒级)较快(有RTT延迟)
数据一致性代码过期前可能不是最新保证最新
控制头Cache-ControlETag/Last-Modified

三 缓存流程

用户访问网站
    ↓
浏览器解析URL
    ↓
检查缓存策略
    ↓
内存缓存? → 是 → 200 (from memory cache)
    ↓ 否
磁盘缓存? → 是 → 缓存过期? → 否 → 200 (from disk cache)
    ↓ 是                ↓
    ↓               协商缓存验证
    ↓                ↓
发起网络请求    服务器返回304200
    ↓
DNS解析 → TCP连接 → TLS握手(HTTPS) → HTTP请求
    ↓
服务器处理请求
    ↓
返回响应 + 缓存头
    ↓
浏览器缓存资源
    ↓
渲染页面

四 前端后端都需要什么

4.1 职能

前端 :控制文件名哈希,只需要负责打包生成带有哈希的文件名,确保文件内容变化文件名变化

后端:配置响应头策略,根据文件特征配置HTTP响应头。

4.2 浏览器存储的位置由谁控制?

存储位置是缓存在内存还是磁盘,是由浏览器内核自动管理的,基于资源类型,大小,使用频率的启发式算法

总结: 前端管生成哈希,后端配置策略,浏览器管存储位置

五 整个流程梳理

完整缓存流程总结:

开发时:
前端打包 → 生成带哈希文件 → 部署到服务器

第一次访问:
无缓存 → 全部请求 → 服务器返回 + 缓存头 → 浏览器缓存

后续访问:
1. HTML(协商缓存):条件请求 → 304200
2. 带哈希资源(强缓存):直接读缓存,无请求
3. 新版本发布:
   a. HTML返回新版本
   b. 新资源文件名变化 → 重新下载
   c. 旧资源继续用缓存
4. 一年后:强缓存过期 → 协商缓存验证

以下是详细步骤

1. 前端打包

npm run build

生成结果
- index.html
- app.8a2f1c3e.js
- vendor.a4b5c5d6.js
- style.9e8fdw2.css

2. html自动引入

<!-- 构建工具自动更新引用 -->
<!DOCTYPE html>
<html>
<head>
  <link href="/styles.9e8f7a6b.css" rel="stylesheet">
</head>
<body>
  <script src="/app.8a2f1c3e.js"></script>
</body>
</html>

3. 后端部署

  • 将dist目录部署服务器
  • nginx根据文件名自动应用缓存策略
# nginx.conf
server {
    listen 80;
    server_name example.com;
    
    # 1. HTML文件 - 短时间强缓存(5分钟)
    location = /index.html {
        root /usr/share/nginx/html;
        add_header Cache-Control "public, max-age=300";
        
        # 添加ETag用于协商缓存
        etag on;
    }
    
    # 2. 带哈希的静态资源 - 1年强缓存
    location ~* .[a-f0-9]{8,}.(js|css|png|jpg|jpeg|gif|ico|woff2)$ {
        root /usr/share/nginx/html;
        expires 1y;
        add_header Cache-Control "public, immutable, max-age=31536000";
        
        # 即使有强缓存,也保留ETag
        etag on;
    }
    
    # 3. 普通资源(无哈希)- 协商缓存
    location ~* .(js|css|png|jpg)$ {
        root /usr/share/nginx/html;
        add_header Cache-Control "no-cache";
        etag on;
    }
    
    # 4. API接口
    location /api/ {
        proxy_pass http://backend:3000;
        add_header Cache-Control "no-cache";
        
        # API通常也用ETag
        etag on;
    }
}

4. 浏览器第一次访问

4.1 步骤1: DNS解析 -》TCP连接 -》 TLS握手

4.2 步骤2:请求HTML

# 请求
GET / HTTP/1.1
Host: example.com
User-Agent: Chrome/120.0
Accept: text/html

# 响应
HTTP/1.1 200 OK
Cache-Control: public, max-age=300  ## 桥重点,这里就有缓存的一些规则返回
Content-Type: text/html; charset=utf-8
ETag: "abc123"
Content-Length: 1024

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="/styles.1e2f3g4h.css">
</head>
<body>
  <script src="/main.3a4b5c6d.js"></script>
  < img src="/logo.5d6e7f8g.png">
</body>
</html>

4.3 请求css

# 请求
GET /styles.1e2f3g4h.css HTTP/1.1
Host: example.com

# 响应
HTTP/1.1 200 OK
Cache-Control: public, immutable, max-age=31536000
Content-Type: text/css
ETag: "def456"
Content-Length: 2048
Last-Modified: Mon, 20 Mar 2023 10:00:00 GMT

/* CSS内容 */

4.4 请求js

# 请求
GET /main.3a4b5c6d.js HTTP/1.1
Host: example.com

# 响应
HTTP/1.1 200 OK
Cache-Control: public, immutable, max-age=31536000
Content-Type: application/javascript
ETag: "ghi789"
Content-Length: 4096
Last-Modified: Mon, 20 Mar 2023 10:00:00 GMT

// JavaScript代码

4.5 浏览器缓存资源

// 浏览器内部记录
cache = {
  "https://example.com/": {
    status: "fresh",
    expires: Date.now() + 300000,  // 5分钟后过期
    response: "<html>...",
    headers: { "ETag": "abc123" }
  },
  "https://example.com/styles.1e2f3g4h.css": {
    status: "fresh",
    expires: Date.now() + 31536000000,  // 1年后过期
    response: "/* css */",
    headers: { "ETag": "def456" }
  },
  "https://example.com/main.3a4b5c6d.js": {
    status: "fresh",
    expires: Date.now() + 31536000000,
    response: "// js",
    headers: { "ETag": "ghi789" }
  }
}

4.6 浏览器根据自身策略缓存

  •  styles.1e2f3g4h.css  (15KB) → 内存缓存(小文件)
  •  main.3a4b5c6d.js  (150KB) → 磁盘缓存(大文件)
  •  logo.5d6e7f8g.png  (50KB) → 磁盘缓存

5. 用户刷新

检查缓存 -》 发起条件请求(根据强制缓存和协商缓存发送不同内容,见本章二) -》 服务器会验证 -》 服务器响应

  1. HTML 没有更新, 浏览器会使用缓存中的HTML,状态码304

  2. HTM 有更新

6. 前端发布V2版本

6.1 前端重新打包(资源文件名更新)

6.2 用户访问

关键点:只有HTML和实际发生变化了,才会重新下载(打包后资源文件更新了,触发重新下载)

               未变化的资源文件继续使用强缓存;

六 其他

6.1 如何避免用户访问到旧版本的前端资源?

"采用多层策略:

构建层:用内容哈希命名静态资源(Webpack的contenthash),文件内容变则文件名变。

缓存策略:

  • HTML入口:协商缓存或短时间强缓存(5分钟)
  • 静态资源:长期强缓存(1年)+ immutable标记
  • API数据:按需设置缓存策略

部署策略:先发布静态资源,再更新HTML引用,避免版本不匹配。

兜底方案:Service Worker管理缓存,版本监控提示用户刷新。

这样既获得了缓存的速度优势,又能保证用户及时获取更新。"

核心原则:HTML 必须能及时更新,它是所有资源的入口。只要 HTML 能获取最新版本,它引用的 JS/CSS 文件名就会变化,自然就会请求新资源。