# HTTP 缓存
HTTP 缓存对性能优化的重要性不言而喻,只要命中缓存就不需要重新加载一遍文件,js 执行时间相比下载时间要快的多,如果能优化下载时间,用户体验会提升很多
HTTP 缓存可以分为:
- 强缓存
- 协商缓存
# 强缓存
强缓存优先级较高,HTTP 会优先判断是否命中强缓存。那么 HTTP 是怎么判断的呢。
强缓存是利用 HTTP 头中的 Expires
和 Cache-Control
两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires
和 cache-control
判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。
# Expires
//Expires
expires: Wed, 11 Sep 2019 16:12:18 GMT
expires
是一个时间戳 通过对比时间戳来决定是否命中强缓存
但 expires
是有问题的,它最大的问题在于对“本地时间”的依赖。如果服务端和客户端的时间设置可能不同,或者我直接手动去把客户端的时间改掉,那么 expires
将无法达到我们的预期。
# Cache-Control
// Cache-Control
Cache-Control:max-age=10
HTTP1.1 新增了 Cache-Control 在 Cache-Control 中,我们通过 max-age 来控制资源的有效期。max-age 不是一个时间戳,而是一个时间长度。
- max-age:单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效
- no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜
- no-store:顾名思义就是不使用任何缓存策略。每次访问资源,浏览器都必须请求服务器,并且,服务器不去检查文件是否变化,而是直接返回完整的资源。
- private:专用于个人的缓存,中间代理、CDN 等不能缓存此响应
- public:响应可以被中间代理、CDN 等缓存
- must-revalidate:只要过期就必须回源服务器验证,
- proxy-revalidate:只要求代理的缓存过期后必须验证,客户端不必回源,只验证到代理这个环节就行了
- s-maxage :限定在代理上能够存多久
- no-transform:代理有时候会对缓存下来的数据做一些优化,比如把图片生成 png、webp 等几种格式,方便今后的请求处理。
no-transform
就会禁止这样做
# Pragma
Pragma 只有一个属性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用强缓存,需要与服务器验证缓存是否新鲜,在 3 个头部属性中的优先级最高。
# 协商缓存
当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了 If-Modified-Since
或者 If-None-Match
的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified
或者 ETag
属性。
# Last-Modified/If-Modified-Since
//client
If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT
//Server
Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT
即最后修改时间。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。
第二次发起请求的时候,请求头会带上上一次响应头中的 Last-Modified
的时间,并放到 If-Modified-Since
请求头属性中,服务端根据文件最后一次修改时间和 If-Modified-Since
的值进行比较,如果相等,返回 304 ,并加载浏览器缓存。
缺点:
- 由于 Last-Modified 修改时间是 GMT 时间,只能精确到秒,如果文件在 1 秒内有多次改动,服务器并不知道文件有改动,浏览器拿不到最新的文件
- 如果服务器上文件被多次修改了但是内容却没有发生改变,服务器需要再次重新返回文件。
# ETag
ETag
就是为了解决 Last-Modified
无法解决高频修改文件缓存问题,可以称他为 文件指纹。
Etag
是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag
就是不同的,反之亦然。因此 Etag
能够精准地感知文件的变化。
ETag 还有强
、弱
之分。
强 ETag
要求资源在字节级别必须完全相符,弱 ETag
在值前有个“W/”标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。
缺点:
- 显而易见,
ETag
需要去检查文件字节变化,然后生成 hash 字符串,增加了运算成本。
//client
If-None-Match: W/"2a3b-1602480f459"
//Server
ETag: W/"2a3b-1602480f459"
Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。
# 整体流程

当浏览器再次访问一个已经访问过的资源时,它会这样做:
- 看看是否命中强缓存,如果命中,就直接使用缓存了。(express 和 Cache-Control:max-age)
- 如果没有命中强缓存,就发请求到服务器检查是否命中协商缓存。(if-last-modify 和 Etag)
- 如果命中协商缓存,服务器会返回 304 告诉浏览器使用本地缓存。
- 否则,返回最新的资源。
# 缓存位置
当强缓存命中或者协商缓存中服务器返回 304 的时候,会从本地取,那么本地缓存在哪里呢
浏览器中的缓存位置有以下 4 种,按优先级分别是:
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
# Service Worker
Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存
、消息推送
和网络代理
等功能。其中的离线缓存就是 Service Worker Cache
。Service Worker
同时也是 PWA
的重要实现机制
PS:大家注意 Server Worker 对协议是有要求的,必须以 https 协议为前提。
# Memory Cache 和 Disk Cache
Memory Cache
顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
Disk Cache
也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache
胜在容量和存储时效性上。
--- | memory cache | disk cache |
---|---|---|
相同点 | 只能存储一些派生类资源文件 | 只能存储一些派生类资源文件 |
不同点 | 退出进程时数据会被清除 | 退出进程时数据不会被清除 |
存储资源-scale | 一般脚本、字体、图片会存在内存当中 | 般非脚本会存在内存当中,如 css 等 |
- 比较大的 JS、CSS 文件会直接被丢进磁盘,反之丢进内存
- 内存使用率比较高的时候,文件优先进入磁盘
# Push Cache
推送缓存
是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在 Chrome 浏览器中只有 5 分钟左右,同时它也并非严格执行 HTTP 头中的缓存指令。
# 用户操作对缓存的影响
# 地址栏输入地址
优先查找Disk Cache
看是否有匹配,没有则发送网络请求
# F5 刷新
优先查找 Memory Cache
然后再去匹配 Disk Cache
。不走强缓存但是
# CTRL+F5 刷新
不使用缓存
# 缓存场景
对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略
对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存 对于频繁变动的资源,可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新 对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件
# 参考资料
《图解 HTTP 缓存》 (opens new window)