你应该知道的浏览器缓存知识

0. 前言

浏览器缓存作为性能优化的重要一环,对于前端而言,重要性不言而喻。之前被人问起浏览器缓存的知识,感觉自己有点一知半解,所以这次好好整理总结了一下。

1. 浏览器缓存分类

目前主流的浏览器缓存分为两类,强缓存和协商缓存,它们的匹配流程如下:

(1)浏览器发送请求前,根据请求头的 expires 和 cache-control 判断是否命中强缓存策略,如果命中,直接从缓存获取资源,并不会发送请求。如果没有命中,则进入下一步。

(2)没有命中强缓存规则,浏览器会发送请求,根据请求头的 last-modified 和 etag 判断是否命中协商缓存,如果命中,直接从缓存获取资源。如果没有命中,则进入下一步。

(3)如果前两步都没有命中,则直接从服务端获取资源。

2. 强缓存

2.1 强缓存原理

强缓存需要服务端设置 expires 和 cache-control。

nginx 代码参考,设置了一年的缓存时间:

1
2
3
4
5
6
7
8
9
location ~ .*\.(ico|svg|ttf|eot|woff)(.*) {
proxy_cache pnc;
proxy_cache_valid 200 304 1y;
proxy_cache_valid any 1m;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_use_stale updating error timeout invalid_header http_500 http_502;
expires 1y;
}

alt

(1)expires:从图可以看出,expires 的值是一个绝对时间,是 http1.0 的功能。如果浏览器的时间没有超过这个 expires 的时间,代表缓存还有效,命中强缓存,直接从缓存读取资源。不过由于存在浏览器和服务端时间可能出现较大误差,所以在之后 http1.1 提出了 cache-control。

(2)cache-control:从图可以看出,cache-control 的值是类似于max-age=31536000这样的,是一个相对时间,31536000 是秒数,正好是一年的时间。当浏览器第一次请求资源的时候,会把 response header 的内容缓存下来。之后的请求会先从缓存检查该 response header,通过第一次请求的 date 和 cache-control 计算出缓存有效时间。如果浏览器的时间没有超过这个缓存有效的时间,代表缓存还有效,命中强缓存,直接从缓存读取资源。

两者可以同时设置,但是优先级 cache-control > expires。

2.2 from disk cache 和 from memory cache

Chrome 在高版本更新了缓存策略(具体哪个我忘了),原来的from cache变成了from disk cache(磁盘缓存)from memory cache(内存缓存)两类,两者有什么区别呢?

先从官方文档来看下:

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, call handlerBehaviorChanged() 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 call handlerBehaviorChanged() after registering or unregistering an event listener.

我的渣六级英语翻一下,大概就是内存缓存是和渲染进程绑定的,大部分情况下于浏览器 Tab 对应。为此我实验了一下:

首次打开 Tab:

alt

刷新(cmd+r)Tab:

alt

可以看到,在命中强缓存的情况下,进程初次渲染会从磁盘读取缓存资源。Chrome 会将部分资源保存到内存中(具体保存的逻辑还不清楚,如果有知道的请告知)。

由于内存缓存是直接从内存中读取的,所以速度更快,从图中可以看出时间是 0ms。而磁盘缓存还需要从磁盘中读取,速度还和磁盘的 I/O 有关,时间大概在 2 ~ 10ms,也是相当快的了。

2.3 强缓存作用

强缓存作为性能优化中缓存方面最有效的手段,能够极大的提升性能。由于强缓存不会向服务端发送请求,对服务端的压力也是大大减小。

对于不太经常变更的资源,可以设置一个超长时间的缓存时间,比如一年。浏览器在首次加载后,都会从缓存中读取。

但是由于不会向服务端发送请求,那么如果资源有更改的时候,怎么让浏览器知道呢?现在常用的解决方法是加一个?v=xxx的后缀,在更新静态资源版本的时候,更新这个 v 的值,这样相当于向服务端发起一个新的请求,从而达到更新静态资源的目的。

3. 协商缓存

3.1 协商缓存原理

在强缓存没有命中的时候,就是协商缓存发挥的地盘了。协商缓存会根据[last-modified/if-modified-since]或者[etag/if-none-match]来进行判断缓存是否过期。

nginx 代码参考:

1
2
3
4
5
6
7
8
9
location ~ .*\.(ico|svg|ttf|eot|woff)(.*) {
proxy_cache pnc;
proxy_cache_valid 200 304 1y;
proxy_cache_valid any 1m;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_use_stale updating error timeout invalid_header http_500 http_502;
etag on;
}

alt

(1)last-modified/if-modified-since:
浏览器首先发送一个请求,让服务端在 response header 中返回请求的资源上次更新时间,就是last-modified,浏览器会缓存下这个时间。然后浏览器再下次请求中,request header 中带上if-modified-since:[保存的last-modified的值]。根据浏览器发送的修改时间和服务端的修改时间进行比对,一致的话代表资源没有改变,服务端返回正文为空的响应,让浏览器中缓存中读取资源,这就大大减小了请求的消耗。由于 last-modified 依赖的是保存的绝对时间,还是会出现误差的情况:一是保存的时间是以秒为单位的,1 秒内多次修改是无法捕捉到的;二是各机器读取到的时间不一致,就有出现误差的可能性。为了改善这个问题,提出了使用 etag。

(2)etag/if-none-match:

etag 是 http 协议提供的若干机制中的一种 Web 缓存验证机制,并且允许客户端进行缓存协商。生成 etag 常用的方法包括对资源内容使用抗碰撞散列函数,使用最近修改的时间戳的哈希值,甚至只是一个版本号。
last-modified一样,浏览器会先发送一个请求得到 etag 的值,然后再下一次请求在 request header 中带上if-none-match:[保存的etag的值]。通过发送的 etag 的值和服务端重新生成的 etag 的值进行比对,如果一致代表资源没有改变,服务端返回正文为空的响应,告诉浏览器从缓存中读取资源。

etag 能够解决 last-modified 的一些缺点,但是 etag 每次服务端生成都需要进行读写操作,而 last-modified 只需要读取操作,从这方面来看,etag 的消耗是更大的。

3.2 协商缓存作用

协商缓存是无法减少请求数的开销的,但是可以减少返回的正文大小。一般来说,对于勤改动的 html 文件,使用协商缓存是一种不错的选择。

4. 刷新缓存方法

刷新强缓存可以使用?v=xxx的后缀。当然,人工更改版本号的成本比较高,而且难以维护,现在主流的是通过 webpack 等打包工具生成[name].[hash].js之类的文件名,也能刷新强缓存。

刷新协商缓存比较简单,修改文件内容即可。

对于浏览器而言,在 Chrome 中,你可以使用审查元素,高版本也叫检查,将 Network 中的 Disable cache 打勾,使用cmd+r刷新页面即可。当然你也可以使用强制刷新,直接在页面使用cmd+shift+r进行刷新。

5. 结尾

以上就是对浏览器缓存的一点拙见,欢迎一起交流。

这篇主要交代了浏览器缓存,下一篇文章内容已经想好了,就是 html5 的离线缓存。