关于缓存,大概可以分为以下几种:
① CDN缓存
② DNS缓存
③ 客户端缓存(无需请求的memory cache,disk cache;需要发请求验证的Etag/Last-Modified304)
④ Service Worker与缓存及离线缓存
⑤ PageCache与ajax缓存
一、 CDN缓存
CDN可以理解为分布在每个县城或乡镇的火车票代售点,用户在浏览网站时,CDN会选择一个离用户最近的CDN边缘节点来响应用户请求。其优势很明显(1)CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;(2)大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源站的负载。
关于CDN缓存,在浏览器本地缓存失效后,浏览器会向CDN边缘节点发起请求。类似浏览器缓存,CDN边缘节点也存在着一套缓存机制。CDN边缘节点缓存策略因服务商不同而不同,但一般都会遵循http标准协议,通过http响应头中的cache-control:max-age字段来设置CDN边缘节点数据缓存时间。
当客户端向CDN节点请求数据时,CDN节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN节点会向源站发出回源请求,从源站拉取最新数据,更新本地缓存,并将最新数据返回给客户端。
CDN服务商一般会提供基于文件后缀、目录多个纬度来指定CDN缓存时间,为用户提供更精细化的缓存管理。
CDN缓存刷新:
CDN边缘节点对开发者是透明的,相比于浏览器ctrl+f5的强制刷新来使浏览器本地缓存失效,开发者可以通过CDN服务商提供的"刷新缓存"接口来达到清理CDN边缘节点缓存的目的。这样开发者在更新数据后,可以使用"刷新缓存"功能来强制CDN节点上地数据缓存过期,保证客户端在访问时,拉倒最新数据。
二、 DNS缓存
DNS(Domain Name System):负责将域名URL转化为服务器主机IP。
DNS查找流程:首先查看浏览器缓存是否存在,不存在则访问本机DNS缓存,再不存在则访问本地DNS服务器。所以DNS也是开销,通常浏览器查找一个给定URL的IP地址要花费20-120ms,在DNS查找完成前,浏览器不能从host那里下载任何东西。
TTL(Time To Live):表示查找返回的DNS记录包含的一个存活时间,过期则这个DNS记录将被抛弃。浏览器DNS缓存也有自己的过期时间,这个时间是独立于本机DNS缓存的,相对也比较短,例如chrome只有1分钟左右。
DNS性能优化最佳实践
当客户端的DNS缓存为空时,DNS查找的数量与web页面中唯一主机名的数量相等。所以减少唯一主机名的数量就可以减少DNS查找的数量。
而有时候需要多设置主机数量来增加DNS点负载均衡,因此减少DNS查找和增加主机数量形成了矛盾关系,经过实战DNS设置2-4个主机名是最佳的。更多负载均衡可以用其他方式实现,例如用nginx做负载均衡。
三、浏览器缓存策略之客户端缓存
1. Cache-control:max-age
假设你的站点引用了一个脚本文件一年都不会改变,那么就希望浏览器将这个脚本文件缓存起来,不用每一次都请求服务器返回相同的内容。这样能够节省带宽开销并且能提升性能。
只需要设置文件返回的http头中的cache-control设置为:cache-control:max-age=31536000。 // 可以控制缓存时间
标准中规定max-age值最大不能超过一年,又因为是以秒为单位,所以值为31536000。
假如这个不变的脚本地址是www.haorooms.com/never-expire.js,那么接下来每次用户请求这个地址时,浏览器都不会再向服务器发出请求,而是直接从本地的浏览器缓存中取。直到一年后或用户手动清除了。
但是期间发现脚本内容需要更改怎么办?改变请求的文件名就好了,例如never-expire-v2.js。
cache-control是http1.1的特性,cache-control还有别的信息:
(1)no-cache:先不要读取缓存中的文件,向web服务器请求验证缓存是否新鲜,新鲜则使用缓存。
(2)no-store:这个字段很关键,它表示数据不在硬盘中临时保存only-if-cached:就是在客户端有缓存时就使用客户端的缓存,这个一般都是在无网时使用
(3)max-stale:只要缓存的时间没有超过max-stale指定的时间,就可以加载使用。可以在无网络的情况下使用must-revalidate:作用相同,但是更为严格,每次请求都校验缓存和服务器源文件,一致就使用缓存,不一致就取最新的。
(4)s-maxage同max-age,覆盖max-age/Expires,但仅适用共享缓存,在私有缓存中被忽略。public表明响应可以呗任何对象(发送请求的客户端、代理服务器等等)缓存。private表明响应只能被单个用户(可能是操作系统用户、浏览器用户)缓存,是非共享的,不能被代理服务器缓存。
2. Expires
设置了Expires也可以避免浏览器和服务器发请求,直到时间过期。
注:Expires是http1.0特性,比cache-control要早,有一些缺陷。由于失效时间是一个绝对时间,所以当客户端本地时间被修改后,服务器与客户端时间偏差变大后,就会导致缓存混乱。
以上两个缓存遵循三级缓存原理,cache-control与expires可以在服务端配置同时启用或者启用任意一个,同时启用的时候cache-control优先级高。
3. Last-Modified 304 协商缓存
服务器为了通知浏览器当前文件的版本,会发送一个上次修改时间的标签,例如:
Last-Modified: Tue, 06 Jan 2018 08:26:32 GMT
假如是304协商缓存,验证步骤如下:
① 浏览器:Hey,我需要jquery.min.js这个文件,如果是在Last-Modified:Tue,06 Jan 2018 08:26:32 GMT之后修改过的,请发给我
② 服务器:检查文件修改时间
③ 服务器:Hey,这个文件在那个时间之后没有被修改过,你已经有最新版本了
④ 浏览器:太好了,那我就显示给用户啦
4. ETag
Etag和304类似,但是级别比Last-Modified高一些
请求过程:
① 浏览器: Hey,我需要haorooms的main.css这个文件,有没有不匹配"61213-1762a-50bf790757204"这个串的
② 服务器:(检查ETag...)
③ 服务器:Hey,我这里的版本也是"61213-1762a-50bf790757204",你已经是最新版本了
④ 浏览器:好,那就可以使用本地缓存了
四、Service Worker与缓存及离线缓存
随着Service Worker(以下简称SW)的普及和规范,可以使用SW提供的缓存接口替代HTTP缓存。当然SW的功能是强大的,除了缓存能力,还能实现离线、数据同步、后台编译等等。
一个标配版的sw缓存代码应该有以下的片段:
const version = '2';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => cache.addAll([
'/styles.css',
'/script.js'
]))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
网络请求首先到达的是SW脚本中,如果未命中再转发给http缓存。
这段代码的意思是,在SW的install阶段我们将script.js和styles.css放入缓存中;而在请求发起的fetch阶段,通过资源的URL去缓存内查找匹配,成功后立即返回,否则走正常的网络请求流程。
但是在install阶段的资源内容是哪里来的?仍然是从http缓存中来的。这样SW缓存机制又可能随着HTTP缓存陷入之前说的版本不一致中。
解决办法是让SW中的请求必须向服务端验证:
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => cache.addAll([
new Request('/styles.css', { cache: 'no-cache' }),
new Request('/script.js', { cache: 'no-cache' })
]))
);
});
目前并非所有的浏览器都支持cache选项的配置,但我们可以通过添加随机数来保证每次请求的URL都不相同,间接的使得缓存失效。
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => Promise.all(
[
'/styles.css',
'/script.js'
].map(url => {
// cache-bust using a random query string
return fetch(`${url}?${Math.random()}`).then(response => {
// fail on 404, 500 etc
if (!response.ok) throw Error('Not ok');
return cache.put(url, response);
})
})
))
);
});
五、PageCache与Ajax可缓存
PageCache是facebook提出的,解决ajax缓存的一种方案。
facebook设计了一个框架来识别一个页面是否来自缓存(猜测:页面首次加载完毕后将所有Ajax的Callback和Result缓存在本地。Facebook页面时基于AJAX获取页面内容,参加BigPipe),若来自缓存,通过ajax来更新所需更新到模块(猜测:通过js预先定义本页面所需带薪的div id及对应的callback handler,并在页面下载时同时下载下来)。
其提到了三种更新类型:增量更新,用户复写(例如用户在页面上回复了一则评论)及跨页更新(例如在消息详细页面将一则消息标识为已读,需将首页的未读消息数进行更新)。核心思路还是依据AJAX进行更新。
(1)增量更新:只要页面来自于缓存,即更新所有预定义的需增量更新的模块
(2)用户复写:通过HistoryManager记录用户操作并在cache页面读取后重放所有被标记为"replayable"的操作
(3)跨页更新:通过服务端Database API发送信号至客户端将过期缓存标识为invaild。获得了缓存过期信息后,通过ajax更新需要更新的信息。
facebook顺带提到一个更新Ajax内容避免页面变化/闪烁的小技巧,就是先将需要更新到地方设置为blank,而非直接更新其内容。