基础篇:浏览器缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。

Web 缓存种类很多:数据库缓存、服务端缓存、代理服务器缓存、CDN 缓存,以及浏览器客户端缓存

Web 缓存分为私有缓存、共享缓存两大类:

  • 共享缓存:一份响应副本可以被多个用户复用。例如 CDN 缓存、代理缓存。
  • 私有缓存:仅属于单个用户。浏览器缓存就属于私有缓存;负责前进后退、页面离线浏览,避免重复发起 HTTP 请求。

浏览器缓存又划分为两大体系:

  • HTTP 缓存:HTTP 传输层面的资源缓存,由服务端设置响应头控制;
  • 本地数据存储:JS API 在浏览器本地读写数据(Cookie、Storage、IndexedDB 等),不属于 HTTP 缓存。

HTTP 缓存

HTTP 缓存:把 HTML、图片、JS、CSS、接口响应等 Web 资源副本保存在浏览器客户端。

查找位置(优先级从上到下)

请求资源时,浏览器按顺序依次查找,全部未命中才真正发起网络请求。

  1. Service Worker
  2. Memory Cache(内存缓存)
  3. Disk Cache(磁盘缓存)
  4. 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-cacheno-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-MatchIf-Modified-Since:本地存有对应 ETag、Last-Modified,且强缓存失效。
  • Cache-Control:普通浏览不会自动带上;仅强制刷新、或者 JS 手动配置 cache 选项才会出现在请求头。
  • 强制刷新 Ctrl+F5:浏览器自动追加 Cache-Control: no-cachePragma: no-cache,不会携带 If-None-MatchIf-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 cachefrom 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-ageExpires
  • max-stale=N:客户端可以接受已经过期最多 N 秒的缓存。
  • min-fresh=N:客户端只接受至少 N 秒有效期的缓存。

强缓存失效方案

强缓存生效的本质:浏览器判断当前时间还没超过资源的过期时间,浏览器直接读取本地缓存,不发网络请求。

强制失效方案:

  • 等待自然过期:等待 max-ageExpires 时间到期;无法立刻生效。
  • 修改资源请求 URL(静态资源最优):文件名添加 hash、版本号。
  • 请求层面绕过(JS 请求可控):增加时间戳随机参数。
  • 服务端调整响应头(只影响后续请求):不会清除用户浏览器已经缓存完毕的旧资源,旧缓存会继续使用直至过期。
  • 用户手动操作:Ctrl+F5 强制刷新、浏览器清除缓存。

协商缓存

强缓存过期之后触发协商缓存。浏览器依旧要发送 HTTP 请求,带上缓存标识给服务器校验。

  • 如果资源没有变化:服务器返回 304 Not Modified不返回响应体,浏览器复用本地缓存。
  • 如果资源已经变更:返回 200 OK,携带全新资源内容。

两套校验头:Last-ModifiedIf-Modified-SinceETagIf-None-Match,两者同时开启,ETag 优先判断

Last-ModifiedIf-Modified-Since

  • 第一次响应返回 Last-Modified: GMT 时间,记录文件在服务器的最后修改时间;
  • 下次请求,请求头带上 If-Modified-Since,值为上次拿到的时间;
  • 服务器对比时间,文件未修改返回 304;修改返回 200。

缺点:精度只有秒级;1 秒内多次修改无法识别;部分场景文件内容没变,但修改时间被改变,造成缓存失效。

ETagIf-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-cachePragma: no-cache;服务器返回全新 200 资源。

本地数据存储

JS 提供的客户端数据持久化 API。包含 Cookie、WebStorage(localStoragesessionStorage)、Web SQL(废弃)、IndexedDB。

Cookie 是服务器下发,保存在浏览器的小段文本,本职是维持 HTTP 会话状态,不是大容量本地存储

  • 上限:单条最大 4KB,域名总 Cookie 数量有限。
  • 特点:同源的每一次 HTTP 请求,浏览器自动把 Cookie 带到请求头,产生额外流量开销
  • 属性:ExpiresMax-Age 过期时间、Secure 仅 HTTPS、HttpOnly 禁止 JS 读取、SameSite 防 CSRF。

Web Storage

HTML5 新增存储,分为 localStoragesessionStorage。容量一般 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。

参考资料

MDN HTTP 缓存

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-21 21:37:12

results matching ""

    No results matching ""