Web缓存总结

为什么使用Web缓存

  1. 减少网络带宽消耗
  2. 降低服务器压力
  3. 减少网络延迟,加快页面打开速度

Web缓存类型

数据库数据缓存

Web应用,特别是SNS类型的应用,往往关系比较复杂,数据库表繁多,如果频繁进行数据库查询,很容易导致数据库不堪重荷。为了提供查询的性能,会将查询后的数据放到内存中进行缓存,下次查询时,直接从内存缓存直接返回,提供响应效率。

服务器端缓存

代理服务器缓存

代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大。可以把它理解为一个共享缓存,不只为一个用户服务,一般为大量用户提供服务,因此在减少相应时间和带宽使用方面很有效,同一个副本会被重用多次。

CDN缓存

CDN(Content delivery networks)缓存,也叫网关缓存、反向代理缓存。CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。虽然这种架构负载均衡源服务器之间的缓存没法共享,但却拥有更好的处扩展性。从浏览器角度来看,整个CDN就是一个源服务器,从这个层面来说,本文讨论浏览器和服务器之间的缓存机制,在这种架构下同样适用。

浏览器端缓存

浏览器缓存根据一套与服务器约定的规则进行工作,在同一个会话过程中会检查一次并确定缓存的副本足够新。如果你浏览过程中,比如前进或后退,访问到同一个图片,这些图片可以从浏览器缓存中调出而即时显现。

Web应用层缓存

应用层缓存指的是从代码层面上,通过代码逻辑和缓存策略,实现对数据,页面,图片等资源的缓存,可以根据实际情况选择将数据存在文件系统或者内存中,减少数据库查询或者读写瓶颈,提高响应效率。

浏览器缓存规则

对于浏览器端的缓存来讲,这些规则是在HTTP协议头和HTML页面的Meta标签中定义的。

不能使用缓存

定义任何时候都不使用缓存,返回的响应码为200。

http头部:

1
Cache-Control: no-store

强制缓存

定义使用缓存,在缓存未过期之前,或未定义强制进行服务器校验,使用浏览器中的保存的缓存,返回的响应码为200。

HTTP头部:

1
2
3
4
Cache-Control: max-age: XXX   // 缓存可以使用XXX秒
Expires: XXX // 缓存可以使用到XXX(日期)
Cache-Control: no-cache // 定义强制进行服务器校验
Pragma: no-cache // 定义强制进行服务器校验(HTTP/1.0 中规定的通用首部)

协商缓存

进行服务器校验时,发现缓存文件未发生改变,使用浏览器中的缓存,返回的响应码为304。

HTTP头部:

1
2
Last-Modified: XXX        // 文件最后修改时间
Etag: XXX // 文件对应的唯一标识符(md5标志)

从服务器获取资源

进行服务器校验时,发现缓存文件已发生改变,使用服务器返回数据,返回的响应码为200。

http首部字段

分类

通用首部字段

通用首部字段:请求报文和响应报文均能使用

字段名称 说明
Cache-Control 控制缓存行为
Pragma HTTP1.0遗留,值为“no-cache”禁止缓存

请求首部字段

字段名称 说明
if-Match 比较ETag是否一致
if-None-Match 比较ETag是否不一致
if-Modified-Since 比较最后资源更新的时间是否一致
if-Unmodified-Since 比较最后资源更新的时间是否不一致

响应首部字段

字段名称 说明
ETag 资源的匹配信息

实体首部字段

字段名称 说明
Expires HTTP1.0遗留,实体主体开始时间
Last-Modified 资源最后一次修改时间

Cache-Control

“Expires时间是相对服务器而言,无法保证和客户端时间统一”的问题,http1.1新增了Cache-Control来定义缓存过期时间,若报文中同时出现了PragmaExpiresCache-Control,会以Cache-Control为准。

Cache-Control请求指令表

参数 说明
Cache-Control: max-age=seconds 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。
Cache-Control: max-stale[=seconds] 表明客户端愿意接收一个已经过期的资源。 可选的设置一个时间 (单位秒),表示响应不能超过的过时时间。
Cache-Control: min-fresh=seconds 表示客户端希望在指定的时间内获取最新的响应。
Cache-control: no-cache 强制所有缓存了该响应的缓存用户,在使用已存储的缓存数据前,发送带验证器的请求到原始服务器
Cache-control: no-store 缓存不应存储有关客户端请求或服务器响应的任何内容。
Cache-control: no-transform 不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-Type等HTTP头不能由代理修改。
Cache-control: only-if-cached 表明如果缓存存在,只使用缓存,无论原始服务器数据是否有更新。

Cache-Control响应指令表

参数 意义
Cache-control: must-revalidate 缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源
Cache-control: no-cache 强制所有缓存了该响应的缓存用户,在使用已存储的缓存数据前,发送带验证器的请求到原始服务器
Cache-control: no-store 缓存不应存储有关客户端请求或服务器响应的任何内容
Cache-control: no-transform 不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-Type等HTTP头不能由代理修改
Cache-control: public 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存
Cache-control: private 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)
Cache-control: proxy-revalidate 与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略
Cache-Control: max-age=seconds 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间
Cache-control: s-maxage=seconds 覆盖max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略

示例

1
2
Cache-Control: no-store
Cache-Control: no-cache, no-store, must-revalidate

缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。

1
Cache-Control: no-cache

每次有请求发出时,缓存会将此请求发到服务器(译者注:该请求应该会带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过期,若未过期(注:实际就是返回304),则缓存才使用本地缓存副本。

1
Cache-Control: max-age=3600, must-revalidate

它意味着该资源是从原服务器上取得的,且其缓存(新鲜度)的有效时间为一小时,在后续一小时内,用户重新访问该资源则无须发送请求。

Last-Modified

服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端。客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码即可。传递标记起来的最终修改时间的请求报文首部字段有:If-Modified-SinceIf-Unmodified-Since

If-Modified-Since: Last-Modified-value

示例为If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT
该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上的一致,则直接回送304 和响应报头即可。
当前各浏览器均是使用的该请求首部来向服务器传递保存的 Last-Modified 值。

If-Unmodified-Since: Last-Modified-value

告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412(Precondition Failed)状态码给客户端。
当遇到下面情况时,If-Unmodified-Since字段会被忽略:

  1. Last-Modified值对上了(资源在服务端没有新的修改);
  2. 服务端需返回2XX和412之外的状态码;
  3. 传来的指定日期不合法

ETag

为了解决上述Last-Modified可能存在的不准确的问题,Http1.1还推出了ETag实体首部字段。服务器会通过某种算法,给资源计算得出一个唯一标识符(比如md5标志),在把资源响应给客户端的时候,会在实体首部加上ETag: 唯一标识符一起返回给客户端。客户端会保留该ETag字段,并在下一次请求时将其一并带过去给服务器。服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。请求报文中有两个首部字段If-None-MatchIf-Match可以带上ETag值。

If-None-Match: ETag-value

示例为If-None-Match: "56fcccc8-1699"
告诉服务端如果ETag没匹配上需要重发资源数据,否则直接回送304和响应报头即可。
当前各浏览器均是使用的该请求首部来向服务器传递保存的ETag值。

If-Match: ETag-value

告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed)状态码给客户端。否则服务器直接忽略该字段。If-Match的一个应用场景是,客户端走PUT方法向服务端请求上传/更替资源,这时候可以通过If-Match传递资源的ETag

如果Last-ModifiedETag同时被使用,则要求它们的验证都必须通过才会返回304,若其中某个验证没通过,则服务器会按常规返回资源实体及200状态码。

HTTP1.0缓存

Prama

Pragma是HTTP/1.0标准中定义的一个header属性,请求中包含Pragma的效果跟在头信息中定义Cache-Control: no-cache相同,但是HTTP的响应头不支持这个属性,所以它不能拿来完全替代HTTP/1.1中定义的Cache-control头。通常定义Pragma以向后兼容基于HTTP/1.0的客户端。
当该字段值为no-cache的时候(事实上现在RFC中也仅标明该可选值),会知会客户端不要对该资源读缓存,即每次都得向服务器发一次请求才行。

Pragma属于通用首部字段,在客户端上使用时,常规要求我们往html上加上这段meta元标签(而且可能还得做些hack放到body后面去):

1
<meta http-equiv="Pragma" content="no-cache">

它告诉浏览器每次请求页面时都不要读缓存,都得往服务器发一次请求才行。 事实上这种禁用缓存的形式用处很有限:

  1. 仅有IE才能识别这段meta标签含义,其它主流浏览器仅能识别Cache-Control: no-store的meta标签(见出处)。
  2. 在IE中识别到该meta标签含义,并不一定会在请求字段加上Pragma,但的确会让当前页面每次都发新请求(仅限页面,页面上的资源则不受影响)。

Expires

有了Pragma来禁用缓存,自然也需要有个东西来启用缓存和定义缓存时间,对http1.0而言,Expires就是做这件事的首部字段。
Expires的值对应一个GMT(格林尼治时间),比如“Mon, 22 Jul 2017 11:12:01 GMT”来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求。
在客户端我们同样可以使用meta标签来知会IE(也仅有IE能识别)页面(同样也只对页面有效,对页面上的资源无效)缓存时间:

1
<meta http-equiv="expires" content="mon, 18 apr 2016 14:30:00 GMT">

如果希望在IE下页面不走缓存,希望每次刷新页面都能发新请求,那么可以把content里的值写为“-1”或“0”。注意的是该方式仅仅作为知会IE缓存时间的标记,你并不能在请求或响应报文中找到Expires字段。如果是在服务端报头返回Expires字段,则在任何浏览器中都能正确设置资源缓存的时间。

通过Pragma禁用缓存,又给Expires定义一个还未到期的时间,刷新页面时发现均发起了新请求,Pragma字段的优先级会更高。

离线应用

离线应用之AppCache

已废弃,使用Service Workers代替
HTML5 提供一种应用程序缓存机制,使得基于web的应用程序可以离线运行。开发者可以使用 Application Cache (AppCache) 接口设定浏览器应该缓存的资源并使得离线用户可用。 在处于离线状态时,即使用户点击刷新按钮,应用也能正常加载与工作。

启用应用缓存

服务端:
在服务器上添加MIME TYPE支,让服务器能够识别manifest后缀的文件,AddType text/cache-manifest manifest

前端页面:

1
2
3
<html manifest="example.appcache">
...
</html>

manifest 特性与 缓存清单(cache manifest) 文件关联,这个文件包含了浏览器需要为你的应用程序缓存的资源(文件)列表。你应当在每一个意图缓存的页面上添加 manifest 特性。浏览器不会缓存不带有manifest 特性的页面,除非这个页面已经被写在清单文件内的列表里了。你没有必要添加所有你意图缓存的页面的清单文件,浏览器会暗中将用户访问过的并带有 manifest 特性的所有页面添加进应用缓存中。

缓存清单

缓存清单文件中的段落: CACHE, NETWORK,与 FALLBACK

CACHE:
这是缓存文件中记录所属的默认段落。在 CACHE: 段落标题后(或直接跟在 CACHE MANIFEST 行后)列出的文件会在它们第一次下载完毕后缓存起来。

NETWORK:
在 NETWORK: 段落标题下列出的文件是需要与服务器连接的白名单资源。所有类似资源的请求都会绕过缓存,即使用户处于离线状态。可以使用通配符。

FALLBACK:
FALLBACK: 段指定了一个后备页面,当资源无法访问时,浏览器会使用该页面。该段落的每条记录都列出两个 URI—第一个表示资源,第二个表示后备页面。两个 URI 都必须使用相对路径并且与清单文件同源。可以使用通配符。

CACHE, NETWORK, 和 FALLBACK 段落可以以任意顺序出现在缓存清单文件中,并且每个段落可以在同一清单文件中出现多次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CACHE MANIFEST
# v1 2011-08-14
# This is another comment
index.html
cache.html
style.css
image1.png

# Use from network if available
NETWORK:
network.html

# Fallback content
FALLBACK:
/ fallback.html

加载文档

使用AppCache文档加载流程:

  • 如果应用缓存存在,浏览器直接从缓存中加载文档与相关资源,不会访问网络。这会提升文档加载速度。
  • 浏览器检查清单文件列出的资源是否在服务器上被修改。
  • 如果清单文件被更新了, 浏览器会下载新的清单文件和相关的资源。 这都是在后台执行的,基本不会影响到webapp的性能。

存在问题

缓存文件更新控制不灵活:就目前HTML5提供的manifest机制来讲,一个页面只能引用一个manifest页面,而且一旦发现这个manifest改变了,就会把里面所有定义的缓存文件全部重新拉取一遍,不管实际上有没有更新,控制比较不灵活。针对这个问题,也有的同学提出了一些建议,比如把需要缓存的文件分模块切分到不同manifest中,并分开用HTML引用,再使用强大的iframe嵌入到入口页面,这样就当某一个模式需要有更新,不会导致其他模块的文件也重新拉取一遍。

离线应用之Service Workers

AppCache — 看起来是个不错的方法,因为它可以很容易地指定需要离线缓存的资源。但是它假定你使用时会遵循诸多规则,如果你不严格遵循这些规则,会出现很大的问题。客户端和服务器之间加入一个Service Workers,主要是为了实现离线处理和消息推送等,让web app可以和native app开始真正意义上的竞争。

使用限制

1、非主线程
平常浏览器窗口中跑的页面运行的是主JavaScript线程,DOM和window全局变量都是可以访问的。而Service Worker是走的另外的线程,可以理解为在浏览器背后默默运行的一个线程,脱离浏览器窗体,因此,window以及DOM都是不能访问的,此时我们可以使用self访问全局上下文

2、异步
Service Worker设计为完全异步,同步API(如XHR和localStorage)不能在Service Worker中使用。Service workers大量使用Promise,因为通常它们会等待响应后继续,并根据响应返回一个成功或者失败的操作,这些场景非常适合Promise

3、https协议
Service Worker对协议也有要求,必须是https协议的

Service Worker生命周期

  1. Download – 下载注册的JS文件
  2. Install – 安装
  3. Activate – 激活
1
2
3
4
5
self.addEventListener('install', function(event) { /* 安装后... */ });
self.addEventListener('activate', function(event) { /* 激活后... */ });

//响应和拦截各种请求
self.addEventListener('fetch', function(event) { /* 请求后... */ });

目前Service Worker的所有应用都是基于上面3个事件的,例如离线开发中,install用来缓存文件,activate用来缓存更新,fetch用来拦截请求直接返回缓存数据。三者构成了完整的缓存控制结构。

Cache和CacheStorage

Cache直接和请求打交道,CacheStorageCache对象打交道,可以直接使用全局的caches属性访问CacheStorage
CacheCacheStorage的出现让浏览器的缓存类型又多了一个:之前有memoryCache和diskCache,现在又多了个ServiceWorker cache。参见具体API

借助Service Worker和cacheStorage离线开发

1.在页面上注册一个Service Worker,例如:

1
2
3
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./cache.js');
}

2.将cache.js这个JS中复制如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var VERSION = 'v1';

// 缓存
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(VERSION).then(function(cache) {
//把cache.addAll()方法中缓存文件数组换成你希望缓存的文件数组。
return cache.addAll([
'./start.html',
'./static/jquery.min.js',
'./static/mm.jpg'
]);
})
);
});

// 缓存更新
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
// 如果当前版本和缓存版本不一致
if (cacheName !== VERSION) {
return caches.delete(cacheName);
}
})
);
})
);
});

// 捕获请求并返回缓存数据
self.addEventListener('fetch', function(event) {
event.respondWith(caches.match(event.request).catch(function() {
return fetch(event.request);
}).then(function(response) {
caches.open(VERSION).then(function(cache) {
cache.put(event.request, response);
});
return response.clone();
}).catch(function() {
return caches.match('./static/mm.jpg');
}));
});

参考文献