🔥强缓存和协商缓存你还不懂?看完这篇立马通透!🔥

135 阅读7分钟

前言

在现在的Web中,页面加载速度直接影响用户体验、SEO排名,甚至转化率。根据Google的研究,页面加载时间每增加1秒,移动端的跳出率就可能上升20% 。那么,如何让网页加载更快?缓存(Cache) 就是最有效的手段之一!

缓存主要依靠服务器端来实现,在服务器端的响应头中设置一些属性来实现强缓存或者协商缓存,它们决定访问网页时资源如何获取,虽然说是服务器端,但是前端也需要懂一点嘛!多懂一点不是坏事嘻嘻嘻~

强缓存

假如有这样一个情景:你在开发一个网站,比如用户上传了新的头像,后端是更新了,但为什么前端没有显示呢?

这就有可能对图片设置了强缓存

什么是强缓存 ?

强缓存是浏览器缓存机制的核心策略之一,它允许浏览器在本地缓存有效期内直接使用缓存资源,而无需向服务器发送请求,从而大幅提升页面加载速度和减轻服务器压力。

强缓存长什么样 ?

假如有一个文件用了强缓存,那么它在服务器端的设置可能是这样的:

// 静态资源(长期强缓存,适合带哈希的文件名)
app.use('/static', express.static(path.join(__dirname, 'public'), {
maxAge: '1y', // 1年缓存 (等价于 max-age=31536000)
immutable: true, // 声明资源永不变化(适合带哈希的文件)
setHeaders: (res, filePath) => {
// 对 JS/CSS/图片等资源设置强缓存
if (filePath.match(/\.(js|css|png|jpg|webp|avif|woff2)$/)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
    },
}));

基本语法就是:

res.setHeader(
'Cache-Control','max-age:31536000'
)

控制强缓存的响应头

Cache-Control(HTTP/1.1 优先级更高)

  • max-age=秒数
    定义资源的最大缓存时间(从请求时间开始计算)。

    Cache-Control: max-age=3600  // 缓存1小时,max-age 单位为 秒
    
  • public
    允许所有用户(浏览器、CDN、代理服务器)缓存资源。

    Cache-Control: public, max-age=3600
    
  • private
    仅允许浏览器缓存,禁止中间代理/CDN缓存(适用于用户私有数据)。

    Cache-Control: private, max-age=3600
    
  • no-cache
    不使用强缓存,每次请求需向服务器验证(走协商缓存)。

    Cache-Control: no-cache
    
  • no-store
    完全禁用缓存(不存储任何副本,不用强缓存,也不用协商缓存)。

    Cache-Control: no-store
    
  • immutable
    声明资源永不变更,缓存期内浏览器绝不验证(适用于带哈希的文件)。

    Cache-Control: max-age=31536000, immutable
    

Expires(HTTP/1.0 遗留字段)

通过绝对时间指定缓存过期时间(优先级低于 Cache-Control)。

Expires: Wed, 21 Oct 2026 07:28:00 GMT

问题依赖客户端本地时间,若时间不准确会导致缓存失效。

通过以上对于响应头的设置,就可以开启强缓存。

强缓存是怎么工作的 ?

当浏览器第一次请求资源时,服务器会返回 缓存控制响应头(如 Cache-ControlExpires),浏览器根据这些头信息决定是否缓存资源及其有效期。

后续请求时,浏览器会先检查缓存是否过期:

  • 未过期:直接使用本地缓存(200 (from disk cache)),不发送请求
  • 已过期:进入协商缓存(一会讲)阶段(携带 If-Modified-Since 或 If-None-Match 向服务器验证)。

image.png

flowchart TD
    A[浏览器请求资源] --> B{强缓存是否命中?}
    B -->|命中| C[检查缓存位置]
    C --> D{在内存缓存中?}
    D -->|是| E[直接使用内存缓存]
    D -->|否| F{在磁盘缓存中?}
    F -->|是| G[加载磁盘缓存]
    F -->|否| H[异常: 应命中但未找到缓存]  %% 理论不会发生
    B -->|未命中| I[向服务器发起请求]
    I --> J[服务器返回资源 + 缓存头]
    J --> K[浏览器存储缓存]
    K --> L{存储位置决策}
    L -->|小文件/高频访问| M[存入内存缓存]
    L -->|大文件/低频访问| N[存入磁盘缓存]

谁决定数据存到disk或者cache ?

这些都是由浏览器决定的:

因素倾向 Memory(内存)倾向 Disk(磁盘)
资源大小小文件(<1MB)大文件(>1MB)
访问频率高频访问低频访问
缓存策略no-store / no-cachemax-age / immutable
浏览器模式隐私/无痕模式正常模式
存储机制Memory CacheDisk Cache / Service Worker
资源类型HTML、小图片视频、大图、离线 PWA

强缓存的应用场景

通过上面的介绍,你应该明白了强缓存的特点了,它能够设置一个过期时间,在这个时间内会一直存储,不会向服务器请求,所以它适合某些800年不变的资源,比如百度的logo,B站的logo,或者掘金的logo等等

这也就说明了最开始的问题:为什么后端头像更新了前端没更新,因为前端用了强缓存没有向服务器发送请求获取新的资源。

由于强缓存还是太局限了,我们需要一个更灵活的方式来存储资源,我们希望当资源发生改变的时候,前端能一起同步发生改变,于是协商缓存!!!它来了!!!!

协商缓存

什么是协商缓存 ?

协商缓存是浏览器缓存机制的核心策略之一,用于在本地缓存过期后,通过向服务器发送验证请求,确认资源是否变化,从而决定是否继续使用缓存或下载新资源。
它与强缓存(直接不请求)形成互补,适用于频繁更新但体积较大的资源(如API响应、用户数据等)。

协商缓存长什么样 ?

在 Webpack 下是这样:

app.use(express.static(path.join(__dirname, 'dist'), {
etag: true, // 默认开启
lastModified: true, // 默认开启
setHeaders: (res) => {
res.setHeader('Cache-Control', 'no-cache'); // 强制协商缓存
},
}));

在普通服务器端,如果利用(Koa\express)等框架的话,针对于静态资源不需要手动添加判断逻辑,框架会自动感受到静态资源的变化,它会自动比对,返回结果。

如果利用CDN或者Nginx处理的话,也不需要手动添加逻辑。

但是遇到动态资源如json的变化,就需要手动添加逻辑,判断其etag或lastModified是否变化了

协商缓存是如何工作的 ?

首次请求

  • 服务器返回资源,并附加 ETag 或 Last-Modified 响应头。
  • 浏览器缓存资源及这些验证字段。

后续请求(缓存过期后):

  • 浏览器携带 If-None-Match(对应 ETag)或 If-Modified-Since(对应 Last-Modified)向服务器验证。

  • 服务器检查资源是否变化:

    • 未变化 → 返回 304 Not Modified(空响应体),浏览器继续用缓存。
    • 已变化 → 返回 200 OK 和新资源。
%% 协商缓存流程图
flowchart TD
    A[浏览器请求资源] --> B{本地是否有缓存?}
    B -->|无缓存| C[直接向服务器请求资源]
    B -->|有缓存| D[携带缓存标识请求服务器]
    D --> E{检查资源是否变更?}
    
    %% Last-Modified 分支
    E -->|使用 Last-Modified| F[请求头携带 If-Modified-Since]
    F --> G[服务器对比修改时间]
    G -->|未修改| H[返回 304 Not Modified]
    G -->|已修改| I[返回 200 + 新资源]
    
    %% ETag 分支
    E -->|使用 ETag| J[请求头携带 If-None-Match]
    J --> K[服务器对比 ETag]
    K -->|匹配| L[返回 304 Not Modified]
    K -->|不匹配| M[返回 200 + 新资源]

    %% 结果处理
    H --> N[浏览器使用本地缓存]
    I --> O[更新缓存并加载新资源]
    L --> N
    M --> O

控制协商缓存的响应头

ETag / If-None-Match(优先级更高)

  • ETag
    服务器生成的资源唯一标识符(通常基于内容哈希)。

    ETag: "abc123"
    
  • If-None-Match
    浏览器将缓存的 ETag 值发给服务器验证。

    If-None-Match: "abc123"
    

Last-Modified / If-Modified-Since

  • Last-Modified
    服务器返回资源的最后修改时间。

    Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
    
  • If-Modified-Since
    浏览器将缓存的修改时间发给服务器验证。

    If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
    

为什么优先用 ETag

  • Last-Modified 精度仅到秒级,1秒内的修改无法检测。
  • ETag 可基于内容哈希,能精准感知文件变化(如内容不变但修改时间变化时,ETag 更可靠)。

关于内容哈希

我们一般在构建项目时会使用内容哈希来检测文件的变化,它会将一整个文件的内容进行哈希,作为文件的文件名,以便被检测到变化。

假设文件 index.js 内容为 console.log("Hello"),其内容哈希可能是:

index.a1b2c3d4.js

如果文件内容修改为 console.log("World"),哈希会变化:

index.e5f6g7h8.js

Etag通过这样来决定是否向客户端发送新的资源,在客户端中If-None-Match存储之前加载内容的哈希值,在后续请求中回传给服务器端,如果服务器端将EtagIf-None-Match的值进行比较,如果不一样,就更新,一样就不更新,直接返回304 状态码,浏览器收到后直接从缓存拿资源。

强缓存/协商缓存 谁的优先级更高?

其实强缓存的优先级是更高的,如果响应头中设置了'Cache-Control':'max-age=10'或者Expires:'Wed, 21 Oct 2026 07:28:00 GMT',那么浏览器会默认先进行强缓存,如果发现强缓存的时间过期了,就会再进入协商缓存。

当没有缓存头的时候,一般的浏览器如Edge或者Chrome,都会不执行任何缓存。但一些浏览器如Safari,对静态资源(如图片)可能尝试启发式缓存。