技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

前端缓存

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/22-前端缓存
本文目录15 个章节
  1. 一、先区分几种“缓存”
  2. 二、缓存能解决什么问题
  3. 三、HTTP 缓存的基本流程
  4. 四、Expires 和 Cache-Control
  5. 五、协商缓存:Last-Modified 和 ETag
  6. 六、缓存版本更新:内容指纹
  7. 七、Vary、认证和缓存键
  8. 八、CDN 缓存
  9. 九、Service Worker 与 Cache API
  10. 十、Memory Cache、Disk Cache、BFCache 和 HTTP/2 Push
  11. 十一、浏览器本地存储与离线存储
  12. 十二、实际项目的缓存建议
  13. 十三、刷新行为不能绝对化
  14. 总结
  15. 参考资料

前端缓存

Category(分类): Browser, Network, Performance Status: 已更新

缓存是前端性能和部署稳定性中很重要的一环。原文把 HTTP 缓存、CDN、Service Worker、本地存储、DNS 缓存和内存/磁盘缓存放在一起讲,并保留了不少历史实践。本文将这些内容重新整理:过时的结论保留为历史背景,错误的说法直接修正。

一、先区分几种“缓存”

日常说的“前端缓存”至少可能指下面几类东西:

类型保存的内容是否属于 HTTP 缓存典型控制方式
浏览器 HTTP 缓存HTTP 响应(HTML、JS、CSS、图片等)Cache-ControlETagLast-Modified
CDN/反向代理缓存可共享的 HTTP 响应是,共享缓存响应头、CDN 规则、失效 API
Service Worker CacheRequest/Response是一种可编程托管缓存Cache API、Service Worker 策略
Memory/Disk Cache浏览器实现 HTTP 缓存的内部存储是实现细节浏览器自行决定
Cookie少量键值,自动随请求发送Set-Cookie 属性
Web Storage字符串键值localStoragesessionStorage
IndexedDB异步结构化数据IndexedDB 事务
BFCache页面历史快照否,不是资源缓存浏览器自动管理

浏览器缓存、CDN 和源站的关系示意

localStoragesessionStorage 和 Cookie 是客户端存储,不应该笼统地称为 HTTP 缓存。它们适合保存少量状态或业务数据,但不是把所有网络资源“缓存起来”的替代方案;敏感令牌也不能因为放进了本地存储就变得安全。

容量不是固定常数

原文中的“localStorage 约 5 MB、Cookie 约 4 KB、IndexedDB 至少 250 MB”只能作为历史经验,不能当成规范保证:

  • Cookie 的限制通常按单个 Cookie 和请求头大小计算,浏览器和服务端还可能有总量限制;
  • Web Storage 通常是每源几 MB 级别,并且同步读写;
  • IndexedDB、Cache Storage 等配额由浏览器根据磁盘空间、站点使用情况、隐私模式和用户策略动态决定;
  • 存储满、用户清理、浏览器回收和隐私模式都可能导致写入失败。

二、缓存能解决什么问题

优点

  • 减少网络传输和带宽成本;
  • 降低 CDN、源站和数据库的压力;
  • 更快地加载重复访问的资源;
  • 在离线或弱网场景下,通过 Service Worker 提供有限的离线能力;
  • 通过内容指纹和长缓存让静态资源获得较高的缓存命中率。

缺点和风险

  • 过期策略错误会让用户看到旧页面或旧接口;
  • 旧 HTML 和新 JS/CSS 不匹配可能导致白屏;
  • 共享缓存错误地保存个性化响应会造成数据泄露;
  • 缓存占用磁盘、内存或配额;
  • Service Worker 的错误策略可能让错误响应、旧资源和离线页面长期存在;
  • 缓存使服务器暂时失去对某个 URL 的控制,长时间 max-age 的资源不能被源站直接“召回”。

缓存不是越多越好,而是要明确资源的更新频率、是否个性化、是否允许共享以及失败时的降级策略。

三、HTTP 缓存的基本流程

HTTP 缓存流程示意

一次请求大致经历:

  1. 浏览器或中间缓存根据 URL、请求方法、请求头等信息查找已存储响应;
  2. 如果响应仍然 fresh(新鲜),可以直接使用,不需要访问源站;
  3. 如果响应已 stale(陈旧),但有 ETagLast-Modified,发送条件请求进行验证;
  4. 资源未变化时,服务器返回 304 Not Modified,浏览器继续使用已有响应体;
  5. 资源变化时返回 200 和新响应,缓存按照新的响应头更新;
  6. 没有可用缓存或策略要求重新获取时,正常发送网络请求。

“强缓存”和“协商缓存”是前端文章常用的教学分类:

  • 强缓存:在新鲜期内直接复用,不向服务器验证;
  • 协商缓存:缓存已陈旧或被 no-cache 要求验证,发送条件请求,由服务器决定复用还是返回新内容。

HTTP 规范更常用 fresh/stale、validation/revalidation 等术语。强缓存命中时 DevTools 可能显示 200 (from memory cache)200 (from disk cache),但这些显示属于浏览器实现,不是 HTTP 状态码语义。

四、ExpiresCache-Control

1. Expires:历史兼容字段

Expires 绝对时间缓存示意

Expires 使用绝对时间:

Expires: Wed, 22 Nov 2028 08:41:00 GMT

它受客户端时钟偏差影响,HTTP/1.1 后更推荐使用 Cache-Control: max-age。如果两者同时存在,Cache-Control 中的 max-age 通常优先于 Expires

Expires 并没有从协议中消失;为了兼容旧客户端,有时仍会和 Cache-Control 一起发送,但新项目不应只依赖它。

2. Cache-Control:现代缓存主入口

Cache-Control 相对时间缓存示意

常见响应头:

Cache-Control: public, max-age=31536000, immutable

max-age 表示响应从生成时间起允许保持新鲜的秒数。它不是“服务器收到请求后再倒计时”,共享缓存还会依据 Age 等信息计算当前响应年龄。

常见响应指令

指令含义
max-age=N响应在 N 秒内可视为新鲜
s-maxage=N共享缓存(如 CDN)使用的 max-age,通常优先于 max-age
public即使存在某些通常限制共享缓存的条件,也明确允许共享缓存;仍要确认内容可共享
private只能保存到私有缓存,适合个性化响应
no-cache可以存储,但复用前必须向服务器验证
no-store不应存储该响应;不能理解为清除已经存在的旧缓存
must-revalidate陈旧后必须验证,不能在源站不可用时随意复用
immutable内容在新鲜期内不会改变,减少不必要的重新验证
stale-while-revalidate允许在后台验证时短暂返回陈旧响应,需看浏览器/CDN 支持
stale-if-error出错时允许使用陈旧响应,需评估内容风险

常见请求指令

请求中的 Cache-Control 也有自己的语义,不能和响应指令混为一谈:

  • no-cache:要求本次请求不要直接复用未经验证的响应;
  • no-store:要求本次请求和响应不要被缓存;
  • max-age=0:要求不要使用年龄大于 0 的响应,常用于刷新语义;
  • max-stale=N:允许使用不新鲜不超过 N 秒的响应;
  • min-fresh=N:只接受至少还能新鲜 N 秒的响应;
  • only-if-cached:只使用已有缓存,不能满足时可能返回 504
  • no-transform:中间缓存不应改变媒体类型或内容编码等。

3. no-cacheno-store 不是一回事

这是原文反复强调、但很容易被记错的部分:

# 可以保存,但每次复用前验证
Cache-Control: no-cache

# 不要保存这次响应
Cache-Control: no-store

no-cache 并不等于“没有缓存”,它常常会产生带 If-None-MatchIf-Modified-Since 的请求,并可能收到 304no-store 才是禁止保存这次响应的主要指令,但它也不会自动删除浏览器里已经存在的旧响应。

对于登录后的个性化页面,常见组合是:

Cache-Control: private, no-cache

对于密码重置、支付结果等高敏感响应,可以考虑:

Cache-Control: no-store

不过不要把所有响应都设置成 no-store。过度使用会损失 HTTP 缓存和 BFCache 等平台能力;应依据实际隐私和新鲜度要求选择。

五、协商缓存:Last-ModifiedETag

1. Last-Modified / If-Modified-Since

第一次返回资源时:

HTTP/1.1 200 OK
Last-Modified: Tue, 22 Feb 2028 22:00:00 GMT
Cache-Control: no-cache

缓存陈旧后,客户端可能发送:

GET /app.js HTTP/1.1
If-Modified-Since: Tue, 22 Feb 2028 22:00:00 GMT

如果服务器判断资源没有变化,返回:

HTTP/1.1 304 Not Modified

这种方式实现简单,静态文件服务器通常可以直接从文件修改时间生成它。但它存在局限:HTTP 日期精度通常是秒,分布式机器的文件时间也可能不一致,文件内容未变但修改时间改变时会造成无效验证。

2. ETag / If-None-Match

ETag 是服务器生成的实体标签,不要求一定是文件内容哈希,也可以是版本号、大小和修改时间的组合:

HTTP/1.1 200 OK
ETag: "app-2028-03-01-abc123"
Cache-Control: no-cache

再次验证时:

GET /app.js HTTP/1.1
If-None-Match: "app-2028-03-01-abc123"

一致返回 304,不一致返回新资源。ETag 不是加密,也不能独立提供访问控制。

ETag 有强验证和弱验证:

ETag: "strong-value"
ETag: W/"weak-value"

强验证器要求表示的内容在协议要求的意义下完全一致;弱验证器只表示语义上可视为相同,不能用于所有需要字节级一致的场景。弱 ETag 不等于“取文件一部分做哈希”,具体生成方式由服务端决定。

3. 两个验证器同时存在时

现代服务端通常可以同时发送 ETagLast-Modified

ETag: "v42"
Last-Modified: Tue, 22 Feb 2028 22:00:00 GMT

若请求同时携带 If-None-MatchIf-Modified-SinceIf-None-Match 对条件验证具有优先地位。实际框架可能封装了具体判断逻辑,但不能简单写成“ETag 永远绝对优先”或“Last-Modified 已经没用”。Last-Modified 还可用于爬虫、内容管理和诊断。

六、缓存版本更新:内容指纹

对构建产物使用内容指纹是现代前端最常见的更新策略:

app.8d31c2.js
styles.4fa91e.css
logo.23c0aa.svg

资源内容变化,文件名也变化,旧 URL 的长缓存不会影响新版本:

Cache-Control: public, max-age=31536000, immutable

原文介绍了 Webpack 的 hashchunkhashcontenthash

  • hash 通常与整个构建相关,一个文件变化可能让所有文件名变化;
  • chunkhash 根据 chunk 依赖生成,仍可能受共享 chunk 影响;
  • contenthash 根据具体内容生成,更适合让独立 CSS/JS 长期复用。

这些概念仍有学习价值,但 Vite、Rollup、Rspack 和其他构建工具的命名与分包实现可能不同,实际应检查构建产物。

HTML 为什么通常不能一年强缓存

HTML 入口通常引用最新的 JS/CSS 文件名,因此一般设置:

Cache-Control: no-cache

它允许缓存 HTML,但每次使用前验证,版本更新时能拿到新的资源引用。也可以使用较短的 max-age,配合 CDN 失效和原子部署。

如果 HTML 也使用一年 max-age,用户可能长时间拿到旧入口;即使新静态资源已经部署,旧 HTML 仍会请求旧文件。部署时还要避免先发布 HTML 后发布它引用的资源,可以采用资源先发布、HTML 最后切换的原子发布顺序。

七、Vary、认证和缓存键

缓存不能只看 URL。若响应会因为请求头变化,服务端需要使用 Vary 告诉缓存如何区分:

Vary: Accept-Encoding
Vary: Accept-Language

常见用途:

  • Accept-Encoding:区分 Brotli、Gzip 和未压缩响应;
  • Accept-Language:区分语言内容;
  • Accept:区分媒体类型。

不建议轻易使用 Vary: User-Agent,因为 User-Agent 组合很多,会显著降低命中率。若响应个性化,优先使用 Cache-Control: private,不要只因为请求带 Cookie 就假设所有缓存实现都会自动安全隔离。

还要区分浏览器私有缓存、CDN 共享缓存和 CDN 自定义缓存键。不要让含有用户资料、权限、购物车或支付信息的响应进入可被其他用户复用的共享缓存。

八、CDN 缓存

CDN 通常作为共享或托管缓存,用户请求先到边缘节点:

  1. CDN 按域名、路径、查询参数、请求头和自定义规则查找缓存;
  2. 命中且新鲜时直接返回,并可能带 AgeX-Cache 等诊断头;
  3. 未命中或陈旧时回源;
  4. CDN 根据响应头和自身规则保存响应,再返回给用户。

CDN 不只是“找最近服务器”,还涉及路由、TLS、压缩、缓存键、回源、失效、鉴权、Range 请求和源站保护。不同 CDN 的默认策略不同,不能只看浏览器的 Cache-Control

可考虑为共享缓存设置:

Cache-Control: public, max-age=60, s-maxage=600

如果 CDN 支持,也可使用供应商的 CDN-Cache-ControlSurrogate-Control。发生紧急版本回滚时,使用 CDN 的 purge API;不要指望已经命中长 max-age 的客户端主动访问源站。

九、Service Worker 与 Cache API

Service Worker 是独立于页面主线程的 Worker,可以拦截其作用域内的请求并通过 Cache API 实现可编程缓存。它通常要求安全上下文(HTTPS,localhost 开发环境例外)。

它和 HTTP 缓存不是互斥关系:Service Worker 先决定如何响应,内部可能调用 Cache API,也可能调用网络;网络请求本身仍有 HTTP 缓存。

一个版本化缓存示例

const CACHE_NAME = 'app-shell-v3'
const APP_SHELL = [
  '/',
  '/assets/app.8d31c2.js',
  '/assets/app.4fa91e.css'
]

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(APP_SHELL))
  )
})

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys
        .filter(key => key !== CACHE_NAME)
        .map(key => caches.delete(key))
    ))
  )
})

self.addEventListener('fetch', event => {
  if (event.request.method !== 'GET') return

  event.respondWith(
    caches.match(event.request).then(cachedResponse => {
      const networkResponse = fetch(event.request).then(response => {
        if (response.ok && response.type === 'basic') {
          const copy = response.clone()
          caches.open(CACHE_NAME).then(cache => {
            cache.put(event.request, copy)
          })
        }
        return response
      })

      // 这里是 cache-first;内容站也可以采用 network-first。
      return cachedResponse || networkResponse
    })
  )
})

原文示例中缺少 fetch() 的返回值,缓存未命中时会让请求没有响应;上面补上了网络回退。真实项目要继续处理:

  • 缓存失败和网络失败;
  • 不缓存 404/500 和含用户数据的响应;
  • Cache API 中的 Response 只能消费一次,所以缓存前要 clone()
  • 更新提示、skipWaiting()clientsClaim() 的版本切换风险;
  • 清理旧缓存、配额不足和跨源 opaque response;
  • 不把 Service Worker 当成永远在线的后台进程,浏览器可以暂停或终止它。

十、Memory Cache、Disk Cache、BFCache 和 HTTP/2 Push

1. Memory/Disk Cache

DevTools 的 from memory cachefrom disk cache 有助于观察一次测试,但具体保存在哪个位置、查找顺序和是否复用是浏览器实现细节。不能写成固定的:

Service Worker -> Memory Cache -> Disk Cache -> Push Cache

Service Worker 是可编程拦截层,不是所有浏览器都以同样顺序展示;内存和磁盘缓存还会受到资源类型、进程、浏览器版本、设备内存和缓存压力影响。不要依赖“脚本一定在内存、CSS 一定在磁盘”的结论。

2. BFCache 不是资源缓存

Back/Forward Cache 会保存可恢复的页面快照。用户点击后退/前进时,浏览器可能直接恢复页面,不重新下载 HTML,也不一定重新触发缓存验证。页面可以监听:

window.addEventListener('pageshow', event => {
  if (event.persisted) {
    console.log('页面从 BFCache 恢复')
  }
})

unload、某些未关闭的资源、同步 API 等可能影响进入 BFCache。不要把“后退很快”简单解释成 HTTP 缓存命中。

3. HTTP/2 Push

原文把 Push Cache 作为 HTTP/2 缓存层。HTTP/2 Server Push 已被主流浏览器移除或不再推荐,不能作为现代项目的常规缓存策略。现在应使用合理的 HTML 关键路径、preload、HTTP 缓存、CDN 和 103 Early Hints(如果部署链路支持)。

prefetch 是低优先级的资源预取提示,也不是一个独立的“第五级缓存”;浏览器可以忽略它,且预取资源的复用受缓存策略影响。

十一、浏览器本地存储与离线存储

Cookie 更适合会话标识和少量需要随请求发送的信息:

Set-Cookie: __Host-session=RANDOM_ID; Path=/; Secure; HttpOnly; SameSite=Lax
  • HttpOnly 防止 JavaScript 读取,但不能阻止 XSS 发起已认证请求;
  • Secure 只允许通过 HTTPS 发送;
  • SameSite 控制跨站发送并帮助降低 CSRF;
  • Domain 越宽,受影响的子域越多,应谨慎设置;
  • Path 主要是发送范围,不是可靠的安全隔离边界;
  • Max-Age 是相对秒数,Expires 是日期,两者都存在时通常 Max-Age 优先;
  • Cookie 会附加到请求,不能存放大 JSON、图片或密码。

localStorage

  • 同源范围内跨标签页共享;
  • API 同步,频繁读写大数据会阻塞主线程;
  • 字符串存储,需自行序列化;
  • 没有原生 TTL;
  • XSS 可以读取其中的内容,因此不适合存放长期高价值 Token;
  • 容量和持久性受浏览器配额、隐私模式和用户清理影响。

sessionStorage

sessionStorage 按源和顶级浏览上下文隔离。同一标签页刷新仍然存在,标签页关闭后通常释放;新窗口是否复制初始值还与打开方式有关。不要把“同源所有标签页共享”误用于 sessionStorage

IndexedDB

IndexedDB 是异步、事务化、同源的结构化存储,适合离线数据、较大对象和队列。它不等于“无限容量”,写入必须处理 QuotaExceededError、升级阻塞、版本迁移和用户清理。

WebSQL 已废弃,不应在新项目中使用。Cache API 适合 Request/Response 资源,IndexedDB 适合业务数据,两者可以组合但职责不同。

十二、实际项目的缓存建议

静态资源

Cache-Control: public, max-age=31536000, immutable

前提是 URL 使用内容指纹,且构建产物不会被原地覆盖。

HTML 入口

Cache-Control: no-cache
ETag: "html-v42"

也可以使用较短 max-age 并配合 CDN 失效。重点是保证新 HTML 能引用新资源。

个性化 API

Cache-Control: private, no-cache
Vary: Accept-Encoding

如果数据极其敏感或不应落盘:

Cache-Control: no-store

图片和字体

  • 对内容指纹图片和字体使用长缓存;
  • 对不同尺寸使用 srcset,避免缓存原图后发送过多字节;
  • 字体更新时改变 URL,避免旧字体和 CSS 不一致;
  • CDN 缓存前确认响应不含用户私密信息。

十三、刷新行为不能绝对化

开发者文章常把刷新简单归纳成 Ctrl+F5 跳过所有缓存、F5 只验证协商缓存、地址栏回车直接使用磁盘缓存。实际行为与浏览器、平台、开发者工具的 Disable cache、请求缓存模式、Service Worker 和 BFCache 有关。

更可靠的理解是:

  • 正常导航按资源的缓存新鲜度和验证器处理;
  • 普通 reload 通常会要求重新验证或降低复用程度;
  • force reload 会更积极地绕过缓存;
  • DevTools 的 Disable cache 主要影响打开 DevTools 时的网络请求;
  • Service Worker 可能在 HTTP 缓存之前决定响应;
  • 后退/前进可能从 BFCache 恢复页面。

不要把某次 DevTools 中的 200304from disk cache 结果当成所有浏览器的固定规则。

总结

  1. HTTP 缓存、CDN、Service Worker、Cookie、Web Storage、IndexedDB 和 BFCache 是不同机制;
  2. Cache-Control: no-cache 表示复用前验证,no-store 才表示不要保存这次响应;
  3. Expires 是兼容字段,现代缓存主要使用 Cache-Control
  4. ETagLast-Modified 都有价值,条件请求中 If-None-Match 通常优先;
  5. HTML 入口通常短缓存或协商缓存,带内容指纹的 JS/CSS/图片/字体适合长缓存;
  6. CDN 要同时考虑共享缓存、缓存键、个性化数据、回源和失效;
  7. Service Worker 的缓存策略要有版本、回退、清理和更新机制;
  8. Memory/Disk Cache 的查找顺序属于浏览器实现细节,HTTP/2 Push 不应作为现代方案;
  9. 缓存策略必须和部署、版本更新、隐私和错误恢复一起设计。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS