浏览器缓存

浏览器缓存是前端面试里的高频题,本质上考察的是你对 HTTP 缓存策略、资源加载流程、性能优化手段 的理解。
它的核心目标很简单:减少重复请求、降低带宽消耗、提升页面加载速度,并减轻服务端压力。

1. 为什么需要浏览器缓存

如果浏览器每次打开页面都重新请求所有资源,会带来几个明显问题:

  • 首屏速度慢,用户感知卡顿
  • 重复下载相同的 JS、CSS、图片,浪费带宽
  • 服务端承受不必要的高并发请求
  • 弱网环境下页面更容易加载失败

所以,浏览器会尽可能复用本地已经拿到的资源,只在必要时再向服务器确认资源是否有变化。

2. 浏览器缓存的两大类

面试里通常会把浏览器缓存分成两类:

  • 强缓存
  • 协商缓存

它们的区别可以直接记成一句话:

  • 强缓存:浏览器认为资源还没过期,直接用本地缓存,不发请求
  • 协商缓存:浏览器不确定资源是不是最新,会发请求和服务器确认

3. 强缓存

强缓存命中时,请求甚至不会到达业务服务器,在 Chrome DevTools 里通常能看到 from memory cachefrom disk cache

3.1 Expires

Expires 是 HTTP/1.0 的缓存字段,表示一个绝对过期时间。

Expires: Wed, 19 Mar 2026 08:00:00 GMT

它的问题是依赖客户端本地时间,如果用户机器时间不准,就可能出现缓存判断偏差。所以现在更常用的是 Cache-Control

3.2 Cache-Control

Cache-Control 是 HTTP/1.1 中更重要、也更常见的缓存控制字段。

Cache-Control: max-age=31536000

常见取值:

  • max-age=秒数:资源在多少秒内有效
  • public:响应可以被浏览器和代理服务器缓存
  • private:只能被客户端浏览器缓存
  • no-cache:可以缓存,但每次使用前都要向服务器校验
  • no-store:完全不缓存

其中最容易混淆的是:

  • no-cache 不是“不缓存”,而是“缓存前必须重新验证”
  • no-store 才是真的“不落任何缓存”

3.3 强缓存的典型场景

适合做强缓存的资源通常是:

  • 带 hash 的 JS 文件
  • 带 hash 的 CSS 文件
  • 不经常变化的图片、字体等静态资源

比如:

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

这种策略通常配合构建工具生成的文件指纹使用,例如:

app.8f3d2c.js
style.a91b7e.css

文件内容一旦变化,文件名就会变化,浏览器自然会重新请求新资源。

4. 协商缓存

当强缓存失效后,浏览器会进入协商缓存流程,请求服务器确认资源是否变更。

如果资源没变,服务器返回:

HTTP/1.1 304 Not Modified

这时浏览器会继续使用本地缓存内容,只是这次发生了一次网络请求。

协商缓存主要有两套方案。

4.1 Last-Modified / If-Modified-Since

服务器第一次返回资源时:

Last-Modified: Tue, 18 Mar 2026 10:00:00 GMT

下次浏览器请求同一资源时,会自动带上:

If-Modified-Since: Tue, 18 Mar 2026 10:00:00 GMT

服务器会比较资源最后修改时间:

  • 没变,返回 304
  • 变了,返回新资源和新的 Last-Modified

它的优点是简单,缺点也明显:

  • 只能精确到秒
  • 有些文件内容变了,但修改时间不一定可靠
  • 有些场景只是重新生成文件,内容没变,时间却变了

4.2 ETag / If-None-Match

相比时间戳方案,ETag 更准确。服务器会为资源生成一个唯一标识。

首次响应:

ETag: "686897696a7c876b7e"

下次请求:

If-None-Match: "686897696a7c876b7e"

服务器比较标识:

  • 一致,返回 304
  • 不一致,返回新资源和新的 ETag

实际项目里,ETag 往往比 Last-Modified 更可靠,因此很多场景会优先使用它。

5. 缓存优先级和工作流程

一个常见的面试回答可以这样描述:

  1. 浏览器先检查是否命中强缓存
  2. 如果强缓存命中,直接使用本地资源,不发请求
  3. 如果强缓存未命中,再发起请求,走协商缓存
  4. 服务端判断资源是否变化
  5. 如果未变化,返回 304,浏览器使用本地缓存
  6. 如果已变化,返回 200 和最新资源

可以简化理解为:

先看本地能不能直接用
不能直接用,再问服务器能不能继续用
还不能用,才重新下载

6. 浏览器缓存存放位置

前端面试里有时还会继续追问“缓存放在哪”。

常见位置包括:

  • Memory Cache:内存缓存,读取速度快,关闭页面后通常会释放
  • Disk Cache:磁盘缓存,容量更大,适合持久化资源
  • Service Worker Cache:由 Service Worker 接管和缓存,适合离线能力和精细控制

在 DevTools 里常见到:

  • from memory cache
  • from disk cache

这两个只是缓存命中的结果展示,不代表缓存策略只有两种。真正决定是否命中,还是响应头里的缓存规则。

7. 开发中常见的缓存策略

7.1 HTML 文件

HTML 一般不做长期强缓存,因为它负责引用最新的 JS/CSS 文件。

常见策略:

Cache-Control: no-cache

这样浏览器每次都会向服务器确认 HTML 是否更新,但如果没变,依然可以返回 304

7.2 JS / CSS / 图片等静态资源

静态资源通常做长期强缓存,并配合文件 hash:

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

7.3 接口数据

接口是否缓存要看业务场景:

  • 用户信息、订单状态这类实时数据,通常不做浏览器强缓存
  • 字典数据、地区列表、配置项等稳定数据,可以结合业务做短期缓存

需要注意:HTTP 缓存主要针对静态资源最常见,接口缓存通常还会叠加前端内存缓存、客户端存储、CDN 或网关策略。

8. 面试高频追问

8.1 no-cacheno-store 有什么区别?

  • no-cache:可以缓存,但使用前必须校验
  • no-store:不缓存,请求和响应内容都不应被保存

8.2 为什么有了 ETag 还需要 Cache-Control

因为两者解决的问题不同:

  • Cache-Control 决定要不要直接使用本地缓存
  • ETag 决定发请求后,服务器如何判断资源是否变化

前者偏“是否发请求”,后者偏“发请求后如何校验”。

8.3 为什么按了刷新,资源还是可能走缓存?

普通刷新并不等于禁用缓存。浏览器通常会优先遵循缓存策略。
只有强制刷新(例如 Ctrl + F5)才更接近跳过已有缓存重新请求资源,但具体行为也会受浏览器实现影响。

8.4 304 是不是比强缓存更好?

不是。
强缓存优先级更高,性能也更好,因为它连请求都不发。304 虽然不会重复下载资源,但仍然有一次网络往返。

9. Nginx 缓存配置相关知识点

前端面试里如果继续往下问,面试官经常会把“浏览器缓存”和“Nginx 怎么配”连起来问。
这里要先分清两个概念:

  • 浏览器缓存:依赖响应头,比如 Cache-ControlExpiresETag
  • Nginx 代理缓存:依赖 proxy_cache_* 指令,是 Nginx 自己缓存上游响应,不等于浏览器本地缓存

9.1 expires 指令是做什么的

Nginxngx_http_headers_module 可以给响应添加或修改 ExpiresCache-Control
这也是静态资源缓存最常见的配置入口。

有几个关键点可以直接记住:

  • expires 为正数或 0 时,会生成 Cache-Control: max-age=...
  • expires 为负数时,会生成 Cache-Control: no-cache
  • expires off 表示不处理这两个响应头

例如:

location ~* \.(js|css|png|jpg|jpeg|gif|svg|webp|woff2?)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

这段配置适合带 hash 的静态资源,思路是:

  • 文件名变了再重新请求
  • 文件名不变就长期走强缓存

9.2 HTML 为什么一般不能配长期强缓存

HTML 通常是入口文件,它里面会引用最新构建产物。
如果你把 HTML 也配成一年强缓存,用户很可能一直拿到旧入口,导致新 JS 文件永远更新不到。

常见写法:

location = /index.html {
    expires -1;
    add_header Cache-Control "no-cache";
}

这样做的效果是:

  • 浏览器不会直接长期使用旧 HTML
  • 每次访问时会向服务端校验 HTML 是否更新
  • 如果 HTML 没变,依然可能返回 304

9.3 add_header 的常见用法

add_header 用来追加响应头,经常和 Cache-Control 一起使用。

例如:

location /api/ {
    add_header Cache-Control "no-store" always;
    proxy_pass http://backend;
}

这里有两个容易被追问的点:

  • always 表示不管响应状态码是什么,都尽量把这个响应头带上
  • add_header 有继承规则,如果当前层级重新写了 add_header,上层同名配置不一定会自动叠加,所以排查配置时要看清 httpserverlocation 三层关系

9.4 一套常见的前端项目缓存配置

对于前后端分离项目,常见思路是:

  • index.html 走协商缓存
  • 带 hash 的 JS、CSS、图片走长期强缓存
  • 接口响应按业务决定是否缓存

示例:

server {
    listen 80;
    server_name example.com;
    root /usr/share/nginx/html;

    location = /index.html {
        expires -1;
        add_header Cache-Control "no-cache";
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|svg|webp|woff2?)$ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    location / {
        try_files $uri $uri/ /index.html;
    }
}

这套配置很适合 VueReactRspress 这一类静态产物部署。

9.5 proxy_cache 和浏览器缓存不是一回事

很多人提到 “Nginx 缓存” 时,会把 proxy_cache 也混进来。这个要单独分清:

  • expiresadd_header Cache-Control 面向的是浏览器
  • proxy_cache 面向的是 Nginx 到上游服务之间的响应缓存

也就是说:

  • 前者解决“浏览器要不要重新下载”
  • 后者解决“Nginx 要不要重新请求后端”

一个基础示例:

http {
    proxy_cache_path /data/nginx/cache keys_zone=mycache:10m max_size=1g;

    server {
        listen 80;

        location /api/cacheable/ {
            proxy_pass http://backend;
            proxy_cache mycache;
            proxy_cache_valid 200 302 10m;
            proxy_cache_valid 404 1m;
        }
    }
}

这类配置更偏服务端性能优化,适用于:

  • 热点但变化不频繁的接口
  • CMS 内容页
  • 部分列表查询接口

但如果接口带用户身份、权限、购物车、实时状态等信息,就要非常谨慎,否则容易出现缓存串数据的问题。