前言
在现在的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-Control 或 Expires),浏览器根据这些头信息决定是否缓存资源及其有效期。
后续请求时,浏览器会先检查缓存是否过期:
- 未过期:直接使用本地缓存(
200 (from disk cache)),不发送请求。 - 已过期:进入协商缓存(一会讲)阶段(携带
If-Modified-Since或If-None-Match向服务器验证)。
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-cache | max-age / immutable |
| 浏览器模式 | 隐私/无痕模式 | 正常模式 |
| 存储机制 | Memory Cache | Disk 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存储之前加载内容的哈希值,在后续请求中回传给服务器端,如果服务器端将Etag和If-None-Match的值进行比较,如果不一样,就更新,一样就不更新,直接返回304 状态码,浏览器收到后直接从缓存拿资源。
强缓存/协商缓存 谁的优先级更高?
其实强缓存的优先级是更高的,如果响应头中设置了'Cache-Control':'max-age=10'或者Expires:'Wed, 21 Oct 2026 07:28:00 GMT',那么浏览器会默认先进行强缓存,如果发现强缓存的时间过期了,就会再进入协商缓存。
当没有缓存头的时候,一般的浏览器如Edge或者Chrome,都会不执行任何缓存。但一些浏览器如Safari,对静态资源(如图片)可能尝试启发式缓存。