Web缓存机制
摘选自腾讯前端博客AlloyTeam
本篇主要从缓存的定义、作用、分类和工作机制几个方面来介绍常用的web缓存及其原理。
Web缓存作用和类型
什么Web缓存
Web缓存是指一个Web资源(如html页面,图片,js,数据等)存在于Web服务器和客户端(浏览器)之间的副本。缓存会根据进来的请求保存输出内容的副本;当下一个请求来到的时候,如果是相同的URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。比较常见的就是浏览器会缓存访问过网站的网页,当再次访问这个URL地址的时候,如果网页没有更新,就不会再次下载网页,而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新,浏览器才会再次下载网页。至于浏览器和网站服务器是如何标识网站页面是否更新的机制,将在后面介绍。
Web缓存的作用
减少网络带宽消耗
无论对于网站运营者或者用户,带宽都代表着金钱,过多的带宽消耗,只会便宜了网络运营商。当Web缓存副本被使用时,只会产生极小的网络流量,可以有效的降低运营成本。
降低服务器压力
给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,间接降低服务器的压力。同时,搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率,也能有效降低服务器的压力。
减少网络延迟,加快页面打开速度
带宽对于个人网站运营者来说是十分重要,而对于大型的互联网公司来说,可能有时因为钱多而真的不在乎。那Web缓存还有作用吗?答案是肯定的,对于最终用户,缓存的使用能够明显加快页面打开速度,达到更好的体验。
Web缓存的类型
在Web应用领域,Web缓存大致可以分为以下几种类型:
数据库数据缓存
Web应用,特别是SNS类型的应用,往往关系比较复杂,数据库表繁多,如果频繁进行数据库查询,很容易导致数据库不堪重荷。为了提供查询的性能,会将查询后的数据放到内存中进行缓存,下次查询时,直接从内存缓存直接返回,提供响应效率。比如常用的缓存方案有memcached等。
服务器端缓存
- 代理服务器缓存: 代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大。可以把它理解为一个共享缓存,不只为一个用户服务,一般为大量用户提供服务,因此在减少相应时间和带宽使用方面很有效,同一个副本会被重用多次。常见代理服务器缓存解决方案有Squid等,这里不再详述。
- CDN缓存: CDN(Content delivery networks)缓存,也叫网关缓存、反向代理缓存。CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。虽然这种架构负载均衡源服务器之间的缓存没法共享,但却拥有更好的处扩展性。从浏览器角度来看,整个CDN就是一个源服务器,从这个层面来说,本文讨论浏览器和服务器之间的缓存机制,在这种架构下同样适用。
浏览器端缓存
浏览器缓存根据一套与服务器约定的规则进行工作,在同一个会话过程中会检查一次并确定缓存的副本足够新。如果你浏览过程中,比如前进或后退,访问到同一个图片,这些图片可以从浏览器缓存中调出而即时显现。
Web应用层缓存
应用层缓存指的是从代码层面上,通过代码逻辑和缓存策略,实现对数据,页面,图片等资源的缓存,可以根据实际情况选择将数据存在文件系统或者内存中,减少数据库查询或者读写瓶颈,提高响应效率。
Web缓存机制
对于浏览器端的缓存来讲,这些规则是在HTTP协议头和HTML页面的Meta标签中定义的。他们分别从新鲜度和校验值两个维度来规定浏览器是否可以直接使用缓存中的副本,还是需要去源服务器获取更新的版本。
- 新鲜度(过期机制):也就是缓存副本有效期。一个缓存副本必须满足以下条件,浏览器会认为它是有效的,足够新的: 含有完整的过期时间控制头信息(HTTP协议报头),并且仍在有效期内; 浏览器已经使用过这个缓存副本,并且在一个会话中已经检查过新鲜度; 满足以上两个情况的一种,浏览器会直接从缓存中获取副本并渲染。
- 校验值(验证机制):服务器返回资源的时候有时在控制头信息带上这个资源的实体标签Etag(Entity Tag),它可以用来作为浏览器再次请求过程的校验标识。如过发现校验标识不匹配,说明资源已经被修改或过期,浏览器需求重新获取资源内容。
使用缓存有关的http消息报头
详细解释:
Cache-Control 用于控制文件在本地缓存有效时长。最常见的,比如服务器回包:Cache-Control:max-age=600 表示文件在本地应该缓存,且有效时长是600秒(从发出请求算起)。在接下来600秒内,如果有请求这个资源,浏览器不会发出 HTTP 请求,而是直接使用本地缓存的文件。
Last-Modified 是标识文件在服务器上的最新更新时间。下次请求时,如果文件缓存过期,浏览器通过 If-Modified-Since 字段带上这个时间,发送给服务器,由服务器比较时间戳来判断文件是否有修改。如果没有修改,服务器返回304告诉浏览器继续使用缓存;如果有修改,则返回200,同时返回最新的文件。
Cache-Control 通常与 Last-Modified 一起使用。一个用于控制缓存有效时间,一个在缓存失效后,向服务查询是否有更新。
Cache-Control 还有一个同功能的字段:Expires。Expires 的值一个绝对的时间点,如:Expires: Thu, 10 Nov 2015 08:45:11 GMT,表示在这个时间点之前,缓存都是有效的。
Expires 是 HTTP1.0 标准中的字段,Cache-Control 是 HTTP1.1 标准中新加的字段,功能一样,都是控制缓存的有效时间。当这两个字段同时出现时,Cache-Control 是高优化级的。
Etag 也是和 Last-Modified 一样,对文件进行标识的字段。不同的是,Etag 的取值是一个对文件进行标识的特征字串。在向服务器查询文件是否有更新时,浏览器通过 If-None-Match 字段把特征字串发送给服务器,由服务器和文件最新特征字串进行匹配,来判断文件是否有更新。没有更新回包304,有更新回包200。Etag 和 Last-Modified 可根据需求使用一个或两个同时使用。两个同时使用时,只要满足基中一个条件,就认为文件没有更新。
Dom Storage 存储机制
DOM 存储是一套在 Web Applications 1.0 规范中首次引入的与存储相关的特性的总称,现在已经分离出来,单独发展成为独立的 W3C Web 存储规范。 DOM 存储被设计为用来提供一个更大存储量、更安全、更便捷的存储方法,从而可以代替掉将一些不需要让服务器知道的信息存储到 cookies 里的这种传统方法。
DOM Storage 分为 sessionStorage 和 localStorage。localStorage 对象和 sessionStorage 对象使用方法基本相同,它们的区别在于作用的范围不同。sessionStorage 用来存储与页面相关的数据,它在页面关闭后无法使用。而 localStorage 则持久存在,在页面关闭后也可以使用。
sessionStrorage
sessionStorage 是个全局对象,它维护着在页面会话(page session)期间有效的存储空间。只要浏览器开着,页面会话周期就会一直持续。当页面重新载入(reload)或者被恢复(restores)时,页面会话也是一直存在的。每在新标签或者新窗口中打开一个新页面,都会初始化一个新的会话。
<script type="text/javascript">
// 当页面刷新时,从sessionStorage恢复之前输入的内容
window.onload = function(){
if (window.sessionStorage) {
var name = window.sessionStorage.getItem("name");
if (name != "" || name != null){
document.getElementById("name").value = name;
}
}
};
// 将数据保存到sessionStorage对象中
function saveToStorage() {
if (window.sessionStorage) {
var name = document.getElementById("name").value;
window.sessionStorage.setItem("name", name);
window.location.href="session_storage.html";
}
}
</script>
<form action="./session_storage.html">
<input type="text" name="name" id="name"/>
<input type="button" value="Save" onclick="saveToStorage()"/>
</form>
localStorage
Local Storage 的接口、用法与 Session Storage 一样,唯一不同的是:Local Storage 保存的数据是持久性的。当前 PAGE 关闭(Page Session 结束后),保存的数据依然存在。重新打开PAGE,上次保存的数据可以获取到。另外,Local Storage 是全局性的,同时打开两个 PAGE 会共享一份存数据,在一个PAGE中修改数据,另一个 PAGE 中是可以感知到的。
<script>
//通过localStorage直接引用key, 另一种写法,等价于:
//localStorage.getItem("pageLoadCount");
//localStorage.setItem("pageLoadCount", value);
if (!localStorage.pageLoadCount)
localStorage.pageLoadCount = 0;
localStorage.pageLoadCount = parseInt(localStorage.pageLoadCount) + 1;
document.getElementById('count').textContent = localStorage.pageLoadCount;
</script>
<p>
You have viewed this page
<span id="count">an untold number of</span>
time(s).
</p>
Web SQL Database存储机制
H5 也提供基于 SQL 的数据库存储机制,用于存储适合数据库的结构化数据。根据官方的标准文档,Web SQL Database 存储机制不再推荐使用,将来也不再维护,而是推荐使用 AppCache 和 IndexedDB。
<script>
if(window.openDatabase){
//打开数据库,如果没有则创建
var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024);
//通过事务,创建一个表,并添加两条记录
db.transaction(function (tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)');
tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "foobar")');
tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "logmsg")');
});
//查询表中所有记录,并展示出来
db.transaction(function (tx) {
tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) {
var len = results.rows.length, i;
msg = "<p>Found rows: " + len + "</p>";
for(i=0; i<len; i++){
msg += "<p>" + results.rows.item(i).log + "</p>";
}
document.querySelector('#status').innerHTML = msg;
}, null);
});
}
</script>
<div id="status" name="status">Status Message</div>
Application Cache 机制
Application Cache(简称 AppCache)似乎是为支持 Web App 离线使用而开发的缓存机制。它的缓存机制类似于浏览器的缓存(Cache-Control 和 Last-Modified)机制,都是以文件为单位进行缓存,且文件有一定更新机制。但 AppCache 是对浏览器缓存机制的补充,不是替代。
<!DOCTYPE html>
<html manifest="demo_html.appcache">
<body>
<script src="demo_time.js"></script>
<p id="timePara"><button onclick="getDateTime()">Get Date and Time</button></p>
<p><img src="img_logo.gif" width="336" height="69"></p>
<p>Try opening <a href="tryhtml5_html_manifest.htm" target="_blank">this page</a>, then go offline, and reload the page. The script and the image should still work.</p>
</body>
</html>
上面 HTML 文档,引用外部一个 JS 文件和一个 GIF 图片文件,在其 HTML 头中通过 manifest 属性引用了一个 appcache 结尾的文件。完整的 manifest 文件,如:
CACHE MANIFEST
# 2012-02-21 v1.0.0
/theme.css
/logo.gif
/main.js
NETWORK:
login.asp
FALLBACK:
/html/ /offline.html
总的来说,浏览器在首次加载 HTML 文件时,会解析 manifest 属性,并读取 manifest 文件,获取 Section:CACHE MANIFEST 下要缓存的文件列表,再对文件缓存。
分析:AppCache 看起来是一种比较好的缓存方法,除了缓存静态资源文件外,也适合构建 Web 离线 App。在实际使用中有些需要注意的地方,有一些可以说是”坑“。
- 要更新缓存的文件,需要更新包含它的 manifest 文件,那怕只加一个空格。常用的方法,是修改 manifest 文件注释中的版本号。如:# 2012-02-21 v1.0.0
- 被缓存的文件,浏览器是先使用,再通过检查 manifest 文件是否有更新来更新缓存文件。这样缓存文件可能用的不是最新的版本。
- 在更新缓存过程中,如果有一个文件更新失败,则整个更新会失败。
- manifest 和引用它的HTML要在相同 HOST。
- manifest 文件中的文件列表,如果是相对路径,则是相对 manifest 文件的相对路径。
- manifest 也有可能更新出错,导致缓存文件更新失败。
- 没有缓存的资源在已经缓存的 HTML 中不能加载,即使有网络。例如:http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/
- manifest 文件本身不能被缓存,且 manifest 文件的更新使用的是浏览器缓存机制。所以 manifest 文件的 Cache-Control 缓存时间不能设置太长。 另外,根据官方文档,AppCache 已经不推荐使用了,标准也不会再支持。现在主流的浏览器都是还支持 AppCache的,以后就不太确定了。
Indexed Database
IndexedDB 也是一种数据库的存储机制,但不同于已经不再支持的 Web SQL Database。IndexedDB 不是传统的关系数据库,可归为 NoSQL 数据库。IndexedDB 又类似于 Dom Storage 的 key-value 的存储方式,但功能更强大,且存储空间更大。
IndexedDB 存储数据是 key-value 的形式。Key 是必需,且要唯一;Key 可以自己定义,也可由系统自动生成。Value 也是必需的,但 Value 非常灵活,可以是任何类型的对象。一般 Value 都是通过 Key 来存取的。
分析:IndexedDB 是一种灵活且功能强大的数据存储机制,它集合了 Dom Storage 和 Web SQL Database 的优点,用于存储大块或复杂结构的数据,提供更大的存储空间,使用起来也比较简单。可以作为 Web SQL Database 的替代。不太适合静态文件的缓存。
以key-value 的方式存取对象,可以是任何类型值或对象,包括二进制。
可以对对象任何属性生成索引,方便查询。
较大的存储空间,默认推荐250MB(分 HOST),比 Dom Storage 的5MB 要大的多。
通过数据库的事务(tranction)机制进行数据操作,保证数据一致性。
异步的 API 调用,避免造成等待而影响体验。
File System API
File System API 是 H5 新加入的存储机制。它为 Web App 提供了一个虚拟的文件系统,就像 Native App 访问本地文件系统一样。由于安全性的考虑,这个虚拟文件系统有一定的限制。Web App 在虚拟的文件系统中,可以进行文件(夹)的创建、读、写、删除、遍历等操作。
浏览器给虚拟文件系统提供了两种类型的存储空间:临时的和持久性的。临时的存储空间是由浏览器自动分配的,但可能被浏览器回收;持久性的存储空间需要显示的申请,申请时浏览器会给用户一提示,需要用户进行确认。持久性的存储空间是 WebApp 自己管理,浏览器不会回收,也不会清除内容。持久性的存储空间大小是通过配额来管理的,首次申请时会一个初始的配额,配额用完需要再次申请。
File System API 提供了一组文件与文件夹的操作接口,有同步和异步两个版本,可满足不同的使用场景。下面通过一个文件创建、读、写的例子,演示下简单的功能与用法。
<script type="text/javascript">
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
//请求临时文件的存储空间
if (window.requestFileSystem) {
window.requestFileSystem(window.TEMPORARY, 5*1024*1024, initFS, errorHandler);
}else{
alert('Sorry! Your browser doesn\'t support the FileSystem API');
}
//请求成功回调
function initFS(fs){
//在根目录下打开log.txt文件,如果不存在就创建
//fs就是成功返回的文件系统对象,fs.root代表根目录
fs.root.getFile('log.txt', {create: true}, function(fileEntry) {
//fileEntry是返回的一个文件对象,代表打开的文件
//向文件写入指定内容
writeFile(fileEntry);
//将写入的内容又读出来,显示在页面上
readFile(fileEntry);
}, errorHandler);
}
//读取文件内容
function readFile(fileEntry)
{
console.log('readFile');
// Get a File object representing the file,
// then use FileReader to read its contents.
fileEntry.file(function(file) {
console.log('createReader');
var reader = new FileReader();
reader.onloadend = function(e) {
console.log('onloadend');
var txtArea = document.createElement('textarea');
txtArea.value = this.result;
document.body.appendChild(txtArea);
};
reader.readAsText(file);
}, errorHandler);
}
//向文件写入指定内容
function writeFile(fileEntry)
{
console.log('writeFile');
// Create a FileWriter object for our FileEntry (log.txt).
fileEntry.createWriter(function(fileWriter) {
console.log('createWriter');
fileWriter.onwriteend = function(e) {
console.log('Write completed');
};
fileWriter.onerror = function(e) {
console.log('Write failed: ' + e.toString());
};
// Create a new Blob and write it to log.txt.
var blob = new Blob(['Hello, World!'], {type: 'text/plain'});
fileWriter.write(blob);
}, errorHandler);
}
function errorHandler(err){
var msg = 'An error occured: ' + err;
console.log(msg);
};
</script>
将上面代码复制到 file_system_api.html 文件中,用 Google Chrome 浏览器打开(现在 File System API 只有 Chrome 43+、Opera 32+ 以及 Chrome for Android 46+ 这三个浏览器支持)。由于 Google Chrome 禁用了本地 HTML 文件中的 File System API功能,在启动 Chrome 时,要加上”—allow-file-access-from-files“命令行参数。
上面截图,左边是 HTML 运行的结果,右边是 Chrome 开发者工具中看到的 Web 的文件系统。基本上 H5的几种缓存机制的数据都能在这个开发者工具看到,非常方便。
移动端 Web 加载性能(缓存)优化
分析完 H5提供的各种缓存机制,回到移动端(针对 Android,可能也适用于 iOS)的场景。现在 Android App 大多嵌入了 Webview 的组件,通过内嵌 Webview 来加载一些H5的运营活动页面或资讯页。这样可充分发挥Web前端的优势:快速开发、发布,灵活上下线。但 Webview 也有一些不可忽视的问题,比较突出的就是加载相对较慢,会相对消耗较多流量。
通过对一些 H5页面进行调试及抓包发现,每次加载一个 H5页面,都会有较多的请求。除了 HTML 主 URL 自身的请求外,HTML外部引用的 JS、CSS、字体文件、图片都是一个独立的 HTTP 请求,每一个请求都串行的(可能有连接复用)。这么多请求串起来,再加上浏览器解析、渲染的时间,Web 整体的加载时间变得较长;请求文件越多,消耗的流量也会越多。我们可综合使用上面说到几种缓存机制,来帮助我们优化 Web 的加载性能。