在前端性能优化领域,浏览器缓存是绕不开的关键技术。它就像浏览器为资源搭建的 “临时仓库”,能让重复访问的页面跳过网络请求,直接从本地加载资源,大幅减少加载时间、降低服务器压力。但缓存并非 “一刀切”,而是分为强缓存和协商缓存两种核心机制,二者的触发逻辑、优先级和应用场景截然不同。今天我们就从原理、代码示例到实际应用,全面拆解浏览器缓存。
一、浏览器缓存的核心价值:为什么需要它?
在正式讲解两种缓存机制前,先明确缓存的核心作用 —— 解决 “重复请求” 的性能浪费问题。当用户第一次访问网页时,浏览器会从服务器下载 HTML、CSS、JS、图片等资源;若用户刷新页面或再次访问,若没有缓存,浏览器会重复发起相同请求,这会导致:
-
加载速度慢:重复下载相同资源,增加页面白屏时间;
-
带宽消耗大:用户流量和服务器带宽被无效占用;
-
服务器压力高:大量重复请求增加服务器负载。
而浏览器缓存通过 “本地存储 + 条件判断”,让资源复用更智能,是前端性能优化中 “性价比最高” 的手段之一。
二、强缓存:无需协商,直接复用
强缓存是优先级最高的缓存机制:当浏览器访问资源时,会先检查本地是否有该资源的 “缓存有效期”,若未过期,则直接从本地加载(完全不发起网络请求),只有过期时才会向服务器请求新资源。
1. 强缓存的实现:两大 HTTP 响应头
强缓存通过服务器返回的HTTP 响应头控制,核心有两个:Cache-Control
(HTTP/1.1)和Expires
(HTTP/1.0,已逐步被替代)。
(1)Cache-Control:现代浏览器的首选
Cache-Control
是当前控制强缓存的主流头,支持多种指令,常见配置如下:
-
max-age=xxx
:缓存有效期,单位为秒(s)。例如max-age=3600
表示资源在 1 小时内有效; -
public
:资源可被浏览器、CDN 等所有中间缓存存储(默认值); -
private
:资源仅能被用户浏览器缓存,CDN 等中间节点无法缓存(常用于用户个性化资源,如登录后的页面); -
no-store
:完全禁用缓存,每次都必须从服务器重新下载(常用于实时性要求极高的资源,如股票行情)。
示例:服务器返回的响应头中包含以下配置,代表资源在 30 分钟内有效:
HTTP/1.1 200 OK
Cache-Control: max-age=1800
Content-Type: text/css
Content-Length: 1024
此时浏览器第一次加载 CSS 后,会将资源存入 “内存缓存”(临时,关闭标签页失效)或 “磁盘缓存”(持久,关闭浏览器仍存在);30 分钟内再次访问该页面,浏览器会直接从本地缓存加载 CSS,Network 面板中该资源的状态码为200 OK (from cache)
。
(2)Expires:HTTP/1.0 的遗留方案
Expires
通过指定一个绝对时间来定义缓存有效期,例如:
HTTP/1.1 200 OK
Expires: Wed, 01 Oct 2025 12:00:00 GMT
Content-Type: image/png
表示资源在 2025 年 10 月 1 日 12:00 前有效。
但Expires
有明显缺陷:它依赖客户端时间(即用户电脑的系统时间),若用户手动修改系统时间(如将时间调后 1 年),会导致缓存提前失效或无限期生效,因此现代项目更推荐使用Cache-Control
。
注意:若
Cache-Control
和
Expires
同时存在,
Cache-Control 优先级更高
。
2. 强缓存的代码示例:Nginx 配置
实际项目中,我们通常通过服务器(如 Nginx)配置Cache-Control
。以下是 Nginx 配置示例,为不同类型的资源设置不同缓存时长:
server {
listen 80;
server_name example.com;
# 静态资源强缓存(7天)
location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|woff2)$ {
root /usr/share/nginx/html;
# 强化缓存头设置
add_header Cache-Control "public, max-age=604800, immutable";
expires 7d;
# 添加版本控制建议
access_log off;
}
# HTML文件缓存策略(优先协商缓存)
location ~* \.html$ {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache, must-revalidate";
# 添加ETag支持协商缓存
etag on;
}
# 新增:API接口不缓存
location ~* \.php$ {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
上述配置中:
-
CSS、JS、图片等静态资源会被缓存 7 天,7 天内重复访问无需请求服务器;
-
HTML 文件设置
no-cache
,表示不启用强缓存,每次访问都会发起请求(但会触发协商缓存)。
三、协商缓存:需与服务器 “商量” 是否复用
当强缓存失效(资源过期或配置了no-cache
)时,浏览器会发起条件请求:携带资源的 “唯一标识” 向服务器询问 “当前资源是否有更新?若没更新,我就用本地缓存”。服务器根据标识判断资源状态,决定返回 “新资源” 或 “复用缓存”。
1. 协商缓存的实现:两组 HTTP 头配对
协商缓存通过 “请求头 + 响应头” 的配对实现,核心有两组:Last-Modified/If-Modified-Since
(基于时间)和ETag/If-None-Match
(基于内容)。
(1)Last-Modified / If-Modified-Since:基于修改时间
- 第一次请求:服务器返回资源时,通过
Last-Modified
头告诉浏览器 “该资源的最后修改时间”,例如:
HTTP/1.1 200 OK
Last-Modified: Tue, 30 Sep 2025 08:30:00 GMT
Content-Type: text/js
浏览器将资源和Last-Modified
时间存入缓存。
- 后续请求:强缓存失效后,浏览器会在请求头中携带
If-Modified-Since
,值为之前保存的Last-Modified
时间,向服务器询问:“资源在这个时间后是否有修改?”
GET /index.js HTTP/1.1
Host: example.com
If-Modified-Since: Tue, 30 Sep 2025 08:30:00 GMT
-
服务器判断:
-
若资源未修改(当前修改时间 ≤
If-Modified-Since
):返回304 Not Modified
,不携带资源内容,浏览器直接复用本地缓存; -
若资源已修改(当前修改时间 >
If-Modified-Since
):返回200 OK
和新资源,并更新Last-Modified
时间。
-
缺陷:若资源内容未变,但修改时间被修改(如手动编辑文件但未改内容),会导致服务器误判 “资源已更新”,返回新资源,浪费带宽。
(2)ETag / If-None-Match:基于内容哈希(更可靠)
为解决 “时间误判” 问题,ETag
应运而生 —— 它是服务器对资源内容计算的唯一哈希值(如 MD5、SHA1),资源内容只要有任何变化,哈希值就会改变。
- 第一次请求:服务器返回资源时,通过
ETag
头返回资源的哈希值,例如:
HTTP/1.1 200 OK
ETag: "5f8d7a3e-1234" # 哈希值示例,格式由服务器决定
Content-Type: text/css
浏览器将资源和ETag
存入缓存。
- 后续请求:强缓存失效后,浏览器在请求头中携带
If-None-Match
,值为之前保存的ETag
,向服务器询问:“当前资源的哈希值是否和这个一致?”
GET /style.css HTTP/1.1
Host: example.com
If-None-Match: "5f8d7a3e-1234"
-
服务器判断:
-
若哈希值一致(资源未变):返回
304 Not Modified
,浏览器复用缓存; -
若哈希值不一致(资源已变):返回
200 OK
和新资源,并更新ETag
。
-
优势:完全基于资源内容判断,避免了 “时间修改但内容未变” 的误判,是更可靠的协商缓存方案。
注意:若两组头同时存在,
ETag/If-None-Match 优先级更高
。
2. 协商缓存的代码示例:Node.js 实现
以下是用 Node.js(Express 框架)实现协商缓存的示例,同时支持Last-Modified
和ETag
:
const express = require('express');
const fs = require('fs');
const path = require('path');
const { createHash } = require('crypto');
const app = express();
const port = 3000;
// 计算文件的ETag(基于文件内容的MD5哈希)
function getETag(filePath) {
const content = fs.readFileSync(filePath);
const md5 = createHash('md5').update(content).digest('hex');
return `"${md5}"`; // ETag通常带双引号
}
// 处理CSS文件请求,启用协商缓存
app.get('/style.css', (req, res) => {
const filePath = path.join(_dirname, 'public', 'style.css');
const stats = fs.statSync(filePath); // 获取文件状态(含修改时间)
const fileETag = getETag(filePath);
const lastModified = stats.mtime.toUTCString(); // 格式化为HTTP时间
// 1. 检查If-None-Match(ETag)
if (req.headers['if-none-match'] === fileETag) {
return res.status(304).end(); // 哈希一致,返回304
}
// 2. 检查If-Modified-Since(Last-Modified)
if (req.headers['if-modified-since'] === lastModified) {
return res.status(304).end(); // 时间未变,返回304
}
// 3. 资源已更新,返回新资源并设置协商缓存头
res.setHeader('ETag', fileETag);
res.setHeader('Last-Modified', lastModified);
res.setHeader('Cache-Control', 'no-cache'); // 禁用强缓存,强制走协商缓存
res.sendFile(filePath);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
运行该服务后,第一次访问http://localhost:3000/style.css
会返回200 OK
和 CSS 内容;刷新页面(强缓存已禁用),浏览器会携带If-None-Match
和If-Modified-Since
请求,若 CSS 未修改,服务器返回304
,浏览器复用本地缓存。
四、强缓存与协商缓存的核心差异
为了更清晰地对比两种缓存机制,我们整理了关键差异点:
对比维度 | 强缓存 | 协商缓存 |
---|---|---|
是否发起请求 | 未过期时不发起任何网络请求 | 每次都会发起请求(但可能不返回内容) |
判断逻辑 | 客户端独立判断(基于缓存有效期) | 服务器判断(基于资源标识) |
状态码 | 未过期:200 (from cache) | 未修改:304;已修改:200 |
核心头 | Cache-Control、Expires | ETag/If-None-Match、Last-Modified/If-Modified-Since |
适用场景 | 长期不变的静态资源(如图片、第三方库) | 频繁更新的资源(如 HTML、业务 JS) |
五、实际项目中的缓存策略建议
- 静态资源(CSS/JS/ 图片):
-
启用强缓存,设置较长有效期(如 7-30 天);
-
结合 “资源指纹”(如文件名加哈希:
style.5f8d7a3e.css
):当资源更新时,文件名哈希变化,浏览器会认为是新资源,自动绕过缓存加载新内容,避免 “缓存生效但资源已更新” 的问题。
- HTML 文件:
-
禁用强缓存(设置
Cache-Control: no-cache
),强制走协商缓存; -
原因:HTML 是页面入口,若 HTML 被强缓存,即使 CSS/JS 已更新,用户仍会加载旧 HTML,导致资源引用错误。
- 实时性资源(如接口数据):
-
禁用缓存(设置
Cache-Control: no-store
),或使用协商缓存(基于数据更新时间); -
避免接口数据被缓存,导致用户看到旧数据。
总结
浏览器缓存是前端性能优化的 “基石”,而强缓存和协商缓存则是缓存机制的 “左右护法”—— 强缓存负责 “快速复用”,减少请求次数;协商缓存负责 “精准更新”,避免资源过期。在实际项目中,我们需要根据资源类型和更新频率,灵活组合两种缓存机制,才能在 “加载速度” 和 “资源新鲜度” 之间找到最佳平衡。