缓存策略
浏览器的缓存策略是依靠 HTTP Header 来实现的,共分为两种:
- 强缓存
- 协商缓存
强缓存
强缓存是指在缓存期间,请求不会发送到服务器,浏览器直接返回缓存结果,需要设置 Header:
- expires
- Cache-Control
expires
expires: Wed, 10 Oct 2020 09:51:00 GMT
expires 是 HTTP/1.0 中用于控制网页缓存的字段,其值代表服务器返回该请求结果的缓存到期时间,也就是说,再次发起同样的请求时,如果客户端时间小于 Expires 的值,浏览器直接返回缓存结果。
由于 expires 是采用客户端时间去和缓存失效时间做对比,但客户端时间是可以做修改的,如果客户端时间和服务端时间并不同步,就会导致强缓存失效,或者时效变少。
所以,在 HTTP/1.1 中增加了 cache-control 头。
cache-control
cache-control 常见值为:
- public:所有内容都将被缓存(客户端和代理服务器都可缓存)
- private:所有内容只有客户端可以缓存,默认为 private
- no-cache:客户端缓存内容,但是否使用缓存需要经过协商缓存来决定
- no-store:所有内容都不会被缓存
- max-age=xxx:缓存内容将在 xxx 秒之后失效
这个例子中,expires 和 cache-control 都被设置了,但是 cache-control 优先级高,所以该资源会在 2592000 秒(也就是 30 天)后失效。
我们可以得出 2 个结论:
- 当 expires 和 cache-control 同时存在时,只有 cache-control 生效。
- 在某些不支持 HTTP/1.1 的环境下,expires 就会发挥用处,现阶段它的存在只是为了兼容性
Memory Cache & Disk Cache
当我们 F12 查看浏览器网络请求的时候,肯定看到过这样的信息,from memory cache(内存缓存)和 from disk cache(磁盘缓存)。
当请求命中强缓存时,浏览器就会从内存或者磁盘中将缓存的资源返回来,请求不会到达服务器。
那么,哪些资源缓存在 memory,哪些缓存在 disk 呢?
关于 memory cache 和 disk cache,Chrome 官方有这么一段描述:
Caching
Chrome employs two caches — an on-disk cache and a very fast in-memory cache. The lifetime of an in-memory cache is attached to the lifetime of a render process, which roughly corresponds to a tab. Requests that are answered from the in-memory cache are invisible to the web request API. If a request handler changes its behavior (for example, the behavior according to which requests are blocked), a simple page refresh might not respect this changed behavior. To make sure the behavior change goes through, callhandlerBehaviorChanged()
to flush the in-memory cache. But don't do it often; flushing the cache is a very expensive operation. You don't need to callhandlerBehaviorChanged()
after registering or unregistering an event listener.
以上引用自 Chrome API
读取 memory 中的缓存资源,肯定要比读取 disk 中的更快,但是 memory 中的缓存,会随着进程的释放而释放,也就是说,一旦我们关闭 Tab 标签,memory 中的缓存也就没有了。
那么哪些资源会被缓存到 memory,哪些会缓存到 disk 中呢?关于这点我也没有找到定论,大多数的观点如下,供大家参考:
- 大文件,优先缓存至 disk,小文件优先缓存至 memory
- 当内存占用率高的情况下,优先缓存至 disk
协商缓存
如果请求没有命中强缓存,或者强缓存失效后,就需要向服务器发起请求,验证资源是否有更新,这个过程叫做协商缓存。
当浏览器发起请求验证资源时,如果资源没有改变,那么服务器返回 304 状态码,并且更新浏览器缓存有效期;如果资源发生改变,那么服务器返回 200 状态码,并且返回相应资源,更新浏览器缓存有效期。
那么服务器如何确定资源有没有更新呢,这里就要用到以下 2 组 HTTP 头。
last-modified & if-modified-since
last-modified 表示文件的最后修改日期,由服务器添加到 Response Header 中;if-modified-since 由浏览器添加到 Request Header 中,是上一次该资源的 last-modified 值。
服务器收到请求后,会将 if-modified-since 和服务器上该文件的修改时间戳进行比对,如果超过了缓存时间,那么则返回最新的资源,200 状态码,如果还在缓存有效期内,则返回 304 状态码。
上面这个例子可以看到:
- Request Header 中 if-modified-since:
Fri, 20 Dec 2019 12:44:01 GMT
- Response Header 中 last-modified:
Fri, 20 Dec 2019 12:44:01 GMT
这里服务器将 if-modified-since 的时间和服务器上文件的修改时间做比对,发现仍在缓存时间有效期内,所以直接返回 304 状态码,并不返回文件资源,由浏览器提供缓存好的资源。
但是 last-modified 也有它的缺点:
- 如果服务器上的文件被打开过,及时没有修改,它的修改时间戳也会改变,就会导致 last-modified / if-modified-since 失效,服务器再次返回同样的资源
- last-modified / if-modified-since 是以秒为单位,如果在秒以内文件发生了修改,那么根据这组 Header 头服务器会认为文件没有修改,依然命中协商缓存,返回老的资源
因为以上这些问题,于是在 HTTP/1.1 出现了 etag / if-none-match。
etag & if-none-match
etag 类似于文件指纹,可以对文件内容做摘要算法,比如 md5,生成的值作为 etag 的值,由服务器添加到 Response Header 中,浏览器再次请求该资源时,会在 Request Header 中添加 if-none-match 头,值为上次 etag 的值,服务器收到请求后,会对请求资源再次做相同的摘要算法,和 if-none-match 值进行比对,如果不一样,说明资源更新了,返回 200 以及更新后的资源文件,如果相同,说明文件没有被修改,则返回 304,由浏览器返回缓存资源。
总结来说,last-modified / if-modified-sice 和 etag / if-none-match,就是将服务器返回的某一个值,由浏览器在发送请求的时候带回去,服务器拿到值后和本地文件的某个属性进行判断,来决定是否返回新的资源,还是由浏览器返回缓存资源,这个过程,就叫做协商缓存。
类似于 expires 和 cache-control,etag / if-none-match 的优先级要比 last-modified / if-modified-since 高。
如果什么缓存策略都没有设置,那么浏览器会采用一个启发式的算法,通常会读取 Response Header 中的 date 头,减去 last-modified 值的 10% 作为缓存时间。
整体流程图
实际场景
学习了上面的缓存策略,在实际场景中我们该如何应用呢?
频繁变动的资源
- 完全不缓存,cache-control: no-store
- 协商缓存,cache-control: no-cache,使浏览器每次请求都会走服务器,然后配合 etag 或者 last-modified 来验证资源是否有效,这样对比完全不缓存来说,虽然无法减少 HTTP 请求到达服务器的次数,但是可以显著减少响应数据的大小
文件
- HTML 文件不设缓存
- CSS、JS以及图片等文件资源,可以设置一个较长的缓存有效期,比如一年,cache-control: max-age=31536000,只有当 HTML 文件引入的文件名发生变化时,才会去下载最新的资源文件,否则就一直使用缓存