基础篇:浏览器缓存
缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。
Web 缓存种类很多:数据库缓存、服务端缓存、代理服务器缓存、CDN 缓存,以及浏览器客户端缓存。
Web 缓存分为私有缓存、共享缓存两大类:
- 共享缓存:一份响应副本可以被多个用户复用。例如 CDN 缓存、代理缓存。
- 私有缓存:仅属于单个用户。浏览器缓存就属于私有缓存;负责前进后退、页面离线浏览,避免重复发起 HTTP 请求。
浏览器缓存又划分为两大体系:
- HTTP 缓存:HTTP 传输层面的资源缓存,由服务端设置响应头控制;
- 本地数据存储:JS API 在浏览器本地读写数据(Cookie、Storage、IndexedDB 等),不属于 HTTP 缓存。
HTTP 缓存
HTTP 缓存:把 HTML、图片、JS、CSS、接口响应等 Web 资源副本保存在浏览器客户端。
查找位置(优先级从上到下)
请求资源时,浏览器按顺序依次查找,全部未命中才真正发起网络请求。
- Service Worker
- Memory Cache(内存缓存)
- Disk Cache(磁盘缓存)
- Push Cache(HTTP/2推送缓存)
Service Worker
Service Worker 是浏览器后台独立运行的脚本线程。
核心特点:
- 完全由 JS 代码控制缓存:自由决定缓存哪些资源、匹配规则、读取逻辑;缓存持久化。
- 流程:注册 SW →
install事件预缓存资源;后续请求触发fetch事件拦截,优先读取自定义缓存,未命中再根据缓存查找优先级去查找数据。 - 最终不管是从 Memory、Disk Cache 获取数据,还是从网络请求中获取的数据,Network 面板依然会标记来源为 Service Worker。
注意:页面必须是 HTTPS 协议(localhost 本地调试除外)。
Memory Cache(内存缓存)
内存缓存,存放本次会话已经加载过的资源(css、js、图片等)。
- 读取速度极快;生命周期很短,关闭 Tab 标签页,内存直接释放。
- 不会只依靠 URL 做匹配,同时校验
Content-Type、CORS 相关头部。 - 内存紧张时,资源会被驱逐到磁盘缓存。
prefetch预加载资源也会存放在内存缓存。
局限:内存容量有限,不可能把所有资源全部放入内存。大文件一般不会进内存缓存。
Disk Cache(磁盘缓存)
磁盘持久化缓存,浏览器缓存体系中覆盖面最大。
- 读取速度慢于内存,但容量大、持久保存,不受 Tab 关闭影响。
- 严格按照 HTTP 响应头判断是否缓存、是否过期。跨页面、跨标签都可以复用。
浏览器存放策略:大文件优先存入磁盘;系统内存吃紧时,资源也会下沉到磁盘缓存。
Push Cache(HTTP/2 推送缓存)
HTTP/2 服务端推送(Server Push):服务器不等浏览器发送请求,主动把预判需要的资源推送给客户端,推送资源存放在 Push Cache。
关键特性:
- Push Cache 绑定HTTP/2 连接,连接一旦关闭,推送缓存全部销毁;就算资源本身是可长期缓存的也一样。
- 缓存条目只能被使用一次,使用完毕就被清除,后续再请求会走正常网络流程。
- 推送缓存匹配时忽略缓存新鲜度(
max-age),可以推送no-cache、no-store资源。 - 多个页面可以复用同一个 HTTP/2 连接,也就共享同一份 Push Cache;但 Safari、Edge 浏览器实现存在较多 bug,兼容性差。
- 浏览器可以发送取消帧,拒绝接收已经存在本地的推送资源。
- 支持推送证书授权范围内的其他域名资源,但受浏览器实现限制。
注意:HTTP/2 Push 浏览器行为差异大,不建议作为核心性能手段;优先使用 <link rel="preload"> 预加载替代。
HTTP 缓存收益
HTTP 缓存是低成本高收益的前端性能优化方案。
- 减少网络请求,缩短页面加载时间,提升用户体验;
- 降低后端服务器压力;
- 节省带宽流量。
注意:缓存不是越大越好。缓存必须在资源更新后可以及时失效,不能永久缓存变化的动态内容。
HTTP 缓存相关头部
| 头部字段 | 类型 | 说明 |
|---|---|---|
Cache-Control |
通用头 | 控制缓存行为,HTTP 1.1 核心 |
Pragma |
请求头 | HTTP 1.0 兼容,Pragma: no-cache |
If-Match |
请求头 | 匹配 ETag 相等 |
If-None-Match |
请求头 | 匹配 ETag 不相等(协商缓存) |
If-Modified-Since |
请求头 | 对比资源最后修改时间(协商缓存) |
If-Unmodified-Since |
请求头 | 对比资源未被修改 |
ETag |
响应头 | 资源唯一标识哈希串 |
Expires |
实体头 | HTTP 1.0,GMT 绝对过期时间 |
Last-Modified |
响应头 | 资源服务器最后修改时间 |
请求头是浏览器自动携带:
If-None-Match、If-Modified-Since:本地存有对应 ETag、Last-Modified,且强缓存失效。Cache-Control:普通浏览不会自动带上;仅强制刷新、或者 JS 手动配置 cache 选项才会出现在请求头。- 强制刷新
Ctrl+F5:浏览器自动追加Cache-Control: no-cache、Pragma: no-cache,不会携带If-None-Match、If-Modified-Since。
HTTP 缓存策略
完整读写流程:
- 触发 Service Worker
fetch拦截,优先读取 SW 自定义缓存; - 未命中,查找 Memory Cache;
- 内存未命中,查找 Disk Cache:
- 如果命中强缓存且未过期,直接读取磁盘缓存,不发网络请求,状态码 200(from disk cache);
- 如果缓存存在但已经过期,进入协商缓存流程,向服务器发请求校验;服务器返回 304 则复用本地缓存;返回 200 使用新资源。
- 全部缓存未命中,发起真实网络请求;
- 拿到响应,满足缓存条件就写入 Disk Cache;
- 同时将响应副本引用存入 Memory Cache(
no-store除外); - 如果 Service Worker 调用了
cache.put(),额外写入 SW 的 Cache Storage。
两大策略:强缓存、协商缓存。两者都依靠 HTTP 响应头控制,强缓存优先级高于协商缓存。
两者核心异同:
- 共同点:强缓存、协商缓存命中时,最终都使用浏览器本地缓存的资源,不从源服务器获取资源主体;
- 不同点:强缓存不产生网络请求;协商缓存需要发送请求,依靠服务器校验确认缓存有效性。
- 协商缓存也未命中:使用服务器返回的最新资源。
使用建议:
- 频繁变化资源(接口、动态页面):
Cache-Control: no-cache,每次请求走协商缓存;减少响应体体积,但请求次数不会减少。 - 很少变动的静态资源(js、css、图片、字体):设置很长的
max-age,例如max-age=31536000;文件名添加 hash 指纹,内容变化则文件名 hash 改变,引用 URL 变化,旧缓存自然失效。
强缓存
命中强缓存:浏览器完全不发送 HTTP 请求,直接读取本地缓存,Network 面板状态码仍然显示 200,size 标注 from memory cache、from disk cache。
实现两套头:Expires(HTTP 1.0)、Cache-Control(HTTP 1.1),同时存在时,Cache-Control 优先级更高。
Expires
HTTP/1.0,服务器返回一个GMT 格式绝对时间,表示资源过期时刻。
Expires: Thu, 31 Dec 2026 23:55:55 GMT
缺陷:完全依赖客户端本地系统时间,修改本机时间会直接破坏缓存判断,现在仅做向下兼容。
Cache-Control
HTTP/1.1,相对时间,单位秒。
Cache-Control: max-age=31536000
Cache-Control: public, max-age=31536000, s-maxage=31536000
常用指令:
public:响应允许客户端、代理 CDN 等所有中间节点缓存。private:仅浏览器客户端可以缓存;代理服务器禁止缓存(默认值)。no-cache:不是禁止缓存,资源会缓存,但每次使用前必须向服务器走协商缓存校验。no-store:完全禁止缓存,强缓存、协商缓存全部关闭,不保存任何副本。max-age=N:缓存有效时长 N 秒,从响应收到那一刻开始计时。s-maxage=N:仅对 CDN、代理服务器生效,会覆盖max-age与Expires。max-stale=N:客户端可以接受已经过期最多 N 秒的缓存。min-fresh=N:客户端只接受至少 N 秒有效期的缓存。
强缓存失效方案
强缓存生效的本质:浏览器判断当前时间还没超过资源的过期时间,浏览器直接读取本地缓存,不发网络请求。
强制失效方案:
- 等待自然过期:等待
max-age、Expires时间到期;无法立刻生效。 - 修改资源请求 URL(静态资源最优):文件名添加 hash、版本号。
- 请求层面绕过(JS 请求可控):增加时间戳随机参数。
- 服务端调整响应头(只影响后续请求):不会清除用户浏览器已经缓存完毕的旧资源,旧缓存会继续使用直至过期。
- 用户手动操作:Ctrl+F5 强制刷新、浏览器清除缓存。
协商缓存
强缓存过期之后触发协商缓存。浏览器依旧要发送 HTTP 请求,带上缓存标识给服务器校验。
- 如果资源没有变化:服务器返回
304 Not Modified,不返回响应体,浏览器复用本地缓存。 - 如果资源已经变更:返回
200 OK,携带全新资源内容。
两套校验头:Last-Modified、If-Modified-Since 与 ETag、If-None-Match,两者同时开启,ETag 优先判断。
Last-Modified、If-Modified-Since
- 第一次响应返回
Last-Modified: GMT 时间,记录文件在服务器的最后修改时间; - 下次请求,请求头带上
If-Modified-Since,值为上次拿到的时间; - 服务器对比时间,文件未修改返回 304;修改返回 200。
缺点:精度只有秒级;1 秒内多次修改无法识别;部分场景文件内容没变,但修改时间被改变,造成缓存失效。
ETag、If-None-Match
- 服务器根据资源内容计算唯一哈希标识,放在响应头
ETag; - 再次请求,请求头携带
If-None-Match: "xxxETag值"; - 服务器对比 ETag,内容没变返回 304;内容变化返回 200。
两者对比:精度,ETag > Last-Modified,可以识别秒级内的内容改动;性能,Last-Modified 开销更小,ETag 需要服务器计算哈希。
分布式服务器(多台机器):保证 Last-Modified 时间一致;建议关闭 ETag,避免不同机器 ETag 不一致导致缓存失效。
用户行为对缓存的影响
- 地址栏输入 URL 访问:优先查找 Disk Cache;命中直接复用,无则网络请求。
- 普通刷新 F5:Tab 没有关闭,优先使用 Memory Cache;其次 Disk Cache;强缓存过期会走协商缓存。
- 强制刷新 Ctrl+F5:完全绕过本地缓存,请求头附带
Cache-Control: no-cache、Pragma: no-cache;服务器返回全新 200 资源。
本地数据存储
JS 提供的客户端数据持久化 API。包含 Cookie、WebStorage(localStorage、sessionStorage)、Web SQL(废弃)、IndexedDB。
Cookie
Cookie 是服务器下发,保存在浏览器的小段文本,本职是维持 HTTP 会话状态,不是大容量本地存储。
- 上限:单条最大 4KB,域名总 Cookie 数量有限。
- 特点:同源的每一次 HTTP 请求,浏览器自动把 Cookie 带到请求头,产生额外流量开销。
- 属性:
Expires、Max-Age过期时间、Secure仅 HTTPS、HttpOnly禁止 JS 读取、SameSite防 CSRF。
Web Storage
HTML5 新增存储,分为 localStorage、sessionStorage。容量一般 5-10MB,不会随 HTTP 请求自动上传给服务器。
localStorage:持久存储,关闭浏览器依然保留;只能手动 JS 清除或者清除浏览器缓存。遵循同源策略。sessionStorage:会话存储;关闭当前标签页数据就销毁;同一个域名不同 Tab 窗口之间不能共享 sessionStorage。
localStorage.setItem('key','value')
localStorage.getItem('key')
localStorage.removeItem('key')
localStorage.clear()
localStorage.key(index)
Web SQL
浏览器端关系型SQL数据库。
W3C 标准废弃,不再维护。Chrome 曾经支持,Firefox、Safari 部分支持,新项目禁止使用。
class MyWebSql {
constructor () {
this.DB = openDatabase('myDB', '1.0', '测试数据库', 2 * 1024 * 1024, d => {})
}
exec (statement, argus = [], callback = function (SQLTransaction, results){}) {
/* transaction 执行数据库操作,操作内容就是正常的数据库的增删改查 */
this.DB.transaction(tx => {
// executeSql 是执行具体的 sql。sql语句, [变量1, 变量2], 执行后的回调)
// 基本操作与实际数据库操作基本一致。
tx.executeSql(statement, argus, callback )
})
}
tCreate () {
this.exec('CREATE TABLE IF NOT EXISTS myTable1 (id unique, desc)')
}
tInsert () {
this.exec('INSERT INTO myTable1 (id, desc) VALUES (1, "第1条记录")');
this.exec('INSERT INTO myTable1 (id, desc) VALUES (?, ?)', [2, '第2条记录']);
this.exec('INSERT INTO myTable1 (id, desc) VALUES (?, ?)', [3, '第3条记录']);
this.exec('INSERT INTO myTable1 (id, desc) VALUES (?, ?)', [4, '第4条记录']);
}
tSelect () {
this.exec('SELECT * FROM myTable1', [], (SQLTransaction, results) => {
const len = results.rows.length
for (let i = 0; i < len; i++) {
console.log(results.rows.item(i).desc)
}
console.log(`查询记录条数: ${len}`)
}, null)
}
tUpdate () {
this.exec('UPDATE myTable1 SET desc="更新第3条记录" WHERE id=3')
this.exec('UPDATE myTable1 SET desc="更新第4条记录" WHERE id=?', [4])
}
tDelete () {
this.exec('DELETE FROM myTable1 WHERE id=1')
this.exec('DELETE FROM myTable1 WHERE id=?', [2])
}
tDrop () {
this.exec('DROP TABLE myTable1')
}
}
var webSql = new MyWebSql()
webSql.tCreate()
webSql.tInsert()
webSql.tSelect()
// webSql.tUpdate()
// webSql.tDelete()
// webSql.tDrop()
IndexedDB
浏览器端非关系型事务数据库,适合存储大量结构化、二进制数据(Blob、ArrayBuffer)。
核心特点:
- 键值对象仓库存储;异步 API,不会阻塞主线程;
- 完整事务支持,出错自动回滚;
- 严格同源隔离;
- 存储容量很大,远大于 Storage,一般没有硬性上限;
- 支持建立索引做查询。
适合:离线应用、大量本地缓存二进制;小量键值优先用 localStorage。