浏览器缓存
浏览器缓存策略详解
一、为什么需要浏览器缓存?
- 提升性能:用户再次访问时,可以直接从本地加载资源,速度远快于从服务器下载,缩短页面加载时间。
- 减少服务器负载:减少了对服务器的请求次数,降低服务器压力。
- 节省带宽:减少了不必要的数据传输。
二、缓存位置
浏览器缓存的资源可能存储在几个不同的位置,优先级从高到低通常是:
- Service Worker Cache:通过 Service Worker 中的 Cache API 进行程序化控制,非常灵活和强大,是 PWA (Progressive Web Apps) 的核心。
- Memory Cache (内存缓存):速度最快,但生命周期短,通常在浏览器标签页关闭后就清除了。用于存储当前页面正在使用或刚下载的资源。
- Disk Cache (磁盘缓存):存储在硬盘上,生命周期较长,可以在多次会话之间甚至浏览器重启后依然存在(只要没过期或被清除)。
- Push Cache (推送缓存):HTTP/2 的服务器推送功能可以将资源推送到这里,但使用和支持情况相对复杂。
我们通常讨论的 HTTP 缓存策略主要影响的是 Memory Cache 和 Disk Cache。
三、HTTP 缓存策略
HTTP 缓存策略通过 HTTP 响应头和请求头来控制。
1. 强缓存 (Strong Caching) - 不向服务器发送请求
强缓存机制下,如果缓存资源未过期,浏览器会直接从本地缓存中读取资源,不会向服务器发送任何 HTTP 请求。
控制强缓存的 HTTP 响应头字段主要有两个:
-
Expires
(HTTP/1.0):- 指定一个绝对的过期日期和时间 (GMT 格式)。例如:
Expires: Wed, 21 Oct 2026 07:28:00 GMT
。 - 缺点:依赖于客户端和服务器端的时间是否同步,如果客户端时间不准确,可能导致缓存提前或延迟失效。
- 指定一个绝对的过期日期和时间 (GMT 格式)。例如:
-
Cache-Control
(HTTP/1.1 - 优先使用):- 提供了更灵活和强大的缓存控制指令,优先级高于
Expires
。 - 常用指令:
max-age=<seconds>
: 指定资源在被视为“新鲜”的最长时间(以秒为单位),这是一个相对时间,从响应生成时开始计算。例如:Cache-Control: max-age=3600
(缓存1小时)。s-maxage=<seconds>
: 类似于max-age
,但仅适用于共享缓存(如 CDN、代理服务器)。其优先级高于max-age
。public
: 表明响应可以被任何缓存(浏览器、CDN、代理)缓存。private
: 表明响应是针对单个用户的,只能被用户的浏览器缓存,不能被共享缓存缓存。no-store
: 禁止浏览器和所有中间缓存存储任何版本的返回响应。每次请求都必须向服务器重新获取完整资源。no-cache
: 资源可以被缓存,但是浏览器在每次使用缓存副本之前,必须向服务器发送请求进行验证 (协商缓存),确认资源是否仍然有效。容易误解为“不缓存”,实际是“使用前验证”。
- 提供了更灵活和强大的缓存控制指令,优先级高于
2. 协商缓存 (Conditional Caching / Validation) - 向服务器发送请求确认
当强缓存过期(例如 max-age
时间已到),或者资源被标记为 Cache-Control: no-cache
时,浏览器会启用协商缓存。
浏览器会向服务器发送一个 HTTP 请求,但这个请求会带上一些特殊的头部字段,让服务器判断浏览器本地缓存的资源是否仍然有效。
- 如果服务器判断资源未改变,会返回一个
304 Not Modified
状态码,并且响应体为空。浏览器收到 304 后,就直接使用本地缓存的副本。这样只传输了很小的头部信息,节省了带宽。 - 如果服务器判断资源已改变,会返回一个
200 OK
状态码,并在响应体中包含新的资源内容。浏览器会使用新资源并更新本地缓存。
控制协商缓存的 HTTP 头部字段主要有两对(服务器响应头 -> 客户端下次请求头):
-
Last-Modified
/If-Modified-Since
:- 服务器在响应中通过
Last-Modified: <date>
告知浏览器资源的最后修改时间。 - 浏览器缓存该资源和这个时间。
- 下次请求该资源时,浏览器在请求头中带上
If-Modified-Since: <last-modified-date>
。 - 服务器比较这个时间和资源的实际最后修改时间。若一致,返回 304;否则返回 200 和新资源。
- 缺点:时间戳只能精确到秒;如果文件内容没变但修改时间变了(或反之),可能导致不准确。
- 服务器在响应中通过
-
ETag
/If-None-Match
(更优):- 服务器在响应中通过
ETag: "<entity-tag>"
(Entity Tag) 提供一个资源特定版本的唯一标识符(通常是内容的哈希值或版本号)。 - 浏览器缓存该资源和这个 ETag。
- 下次请求该资源时,浏览器在请求头中带上
If-None-Match: "<entity-tag>"
。 - 服务器比较浏览器提供的 ETag 和当前资源的 ETag。若一致,返回 304;否则返回 200 和新资源(以及新的 ETag)。
- 优点:比
Last-Modified
更精确,能识别内容是否真的发生了变化。ETag 的优先级通常高于Last-Modified
。如果两者都存在,服务器会优先使用 ETag。
- 服务器在响应中通过
有Etag了 为什么还要用Last-Modified
- 对于Web服务器来说,获取文件的最后修改时间(Last-Modified)是非常直接和低成本的操作,因为文件系统本身就记录了这个信息。相比之下,生成一个基于内容的 ETag(例如通过计算文件的哈希值)可能需要读取整个文件内容,对于大文件或者I/O敏感的系统,这可能会带来额外的开销。
- Last-Modified 提供了一个人类可读的日期时间戳。这在调试缓存问题或简单查看资源何时更新时非常方便,而 ETag 通常是一个不透明的字符串。
- 对于某些动态生成的内容或流式内容,可能很难或不方便为其计算一个稳定且唯一的 ETag。
四、缓存决策流程
浏览器加载资源时的大致缓存决策流程:
- 检查
Cache-Control
是否为no-store
,如果是,则直接请求服务器,不使用任何缓存。 - 否则,检查本地缓存:
a. 查找是否有匹配的缓存资源。
b. 强缓存判断:
i. 检查Cache-Control
的max-age
或Expires
是否仍然有效(未过期)。
ii. 如果有效(新鲜),则直接使用缓存(命中强缓存),状态码通常是200 OK (from memory cache / from disk cache)
。不发送 HTTP 请求。
c. 协商缓存判断(如果强缓存失效或Cache-Control: no-cache
):
i. 向服务器发送 HTTP 请求,带上If-Modified-Since
(基于上次的Last-Modified
) 和/或If-None-Match
(基于上次的ETag
)。
ii. 服务器根据这些头部判断资源是否有更新:
- 未更新:返回304 Not Modified
。浏览器使用本地缓存。
- 已更新:返回200 OK
和新资源。浏览器使用新资源并更新缓存。 - 如果本地没有缓存,则直接向服务器请求完整资源,服务器返回
200 OK
和资源内容,浏览器根据响应头决定如何缓存。
五、用户行为对缓存的影响
- 地址栏回车 / 链接跳转 / 前进后退按钮:正常使用缓存策略。
- F5 / Ctrl+R (Mac: Cmd+R) - 普通刷新:通常会跳过强缓存,直接发起协商缓存请求(即带上
If-Modified-Since
/If-None-Match
)。 - Ctrl+F5 / Ctrl+Shift+R (Mac: Cmd+Shift+R) - 强制刷新:完全忽略所有缓存(强缓存和协商缓存),直接向服务器请求全新资源。请求头中可能会带上
Cache-Control: no-cache
或Pragma: no-cache
。
六、缓存策略建议
- 不常变动的静态资源 (JS, CSS, 图片等):使用长
max-age
(如一年) + 文件名带哈希值 (例如bundle.[contenthash].js
) 进行版本控制。当文件内容改变,哈希值改变,文件名改变,浏览器会请求新文件。 - 经常变动的资源 (如 HTML 文件本身):使用
Cache-Control: no-cache
(使其总是进行协商缓存) 或较短的max-age
并配合ETag
或Last-Modified
。 - 绝对不能缓存的敏感数据:使用
Cache-Control: no-store
。
理解并合理配置缓存策略,对于提升网站性能和用户体验至关重要。
Web Storage 使用场景总结
一、localStorage
(本地持久化存储)
核心特性:数据永久保存(除非手动清除),同源窗口/标签页共享。
主要使用场景:
- 用户偏好设置:
- 网站主题(如暗黑/明亮模式)
- 字体大小、音量等个性化配置
- “不再提示”类消息的状态
- 持久化的应用状态:
- 未登录用户的购物车(若希望关闭浏览器后仍保留)
- 应用教程引导完成状态
- “记住我”的非敏感标识
- 轻量级离线数据:
- 简单PWA或Web应用的离线内容(如笔记、待办事项)
- 缓存不常变的API数据或应用配置
- 性能优化:
- 缓存计算成本高但不常变的数据,减少重复计算/请求
二、sessionStorage
(会话级临时存储)
核心特性:数据仅在当前浏览器标签页会话期间有效,关闭标签页即清除,标签页之间数据隔离。
主要使用场景:
- 临时性用户输入:
- 多步骤表单的临时数据保存(防意外刷新/关闭)
- 当前会话的搜索历史(仅限当前标签)
- 当前会话的应用状态:
- 单页应用(SPA)中当前页面的临时状态(如组件开合、筛选条件)
- 一次性操作成功后的提示信息(刷新或关闭后消失)
- 特定于标签页的数据:
- Web应用在不同标签页打开独立工作区时的状态隔离
- 防止重复提交:
- 存储一次性令牌,用于表单提交验证
共同注意事项:
- 安全:切勿存储敏感信息(如密码、完整Token),易受XSS攻击。
- 类型:只能存字符串,对象/数组需
JSON.stringify()
和JSON.parse()
。 - 大小:通常5-10MB,不适合大量数据。
- API:同步操作,注意对性能的潜在影响。