前言
最近看了下浏览器缓存优化的东西,也翻越了好多文章。发现很多都是涩涩的文字,本菜鸡一看大片的文字就犯晕(一定要改掉这个坏毛病,文档的阅读很重要), 硬着头皮看了一遍,也想着结合实践的方式帮助其他小伙伴们彻底理解 浏览器缓存机制的概念,加深印象。
更为重要的是很多文章开头就是缓存策略,缓存字段开始(Cache-Control, ETag), 有的又会讲到 from memory cache, from disk cache, 起初实在搞不清楚这两者的区别,直接会在心里问出: 强缓存和协商缓存属于硬盘中的缓存(disk cache)吗 ?? 它们又跟内存缓存有什么样的关系 ??
准备(可以先了解概念)
创建一个项目文件夹,添加如下文件
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="/index.js"></script>
</body>
</html>
- index.js
- server.js
const http = require('http');
const fs = require('fs');
http.createServer(function(request, response) {
if (request.url === '/') {
const html = fs.readFileSync('index.html', 'utf-8')
response.writeHead(200, {
'Content-Type': 'text/html',
})
response.end(html)
}
if (request.url === '/index.js') {
const js = fs.readFileSync('index.js', 'utf-8')
response.writeHead(200, {
'Content-Type': 'text/javascript',
'Cache-Control': 'max-age=10'
})
response.end(js)
}
}).listen(8888)
console.log('listening to 8888')
概念理解
缓存一直以来都是性能优化特别重要的一环, 也是最简单高效的,可以用来网络传输消耗
对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求(强缓存),或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来(协商缓存),这样就减少了响应数据。
缓存位置
- Memory Cache(内存缓存)
- Disk Cache(硬盘缓存)
三级缓存原理 (访问缓存优先级)
- 先在内存中查找,如果有,直接加载。
- 如果内存中不存在,则在硬盘中查找,如果有直接加载。
- 如果硬盘中也没有,那么就进行网络请求。
- 请求获取的资源缓存到硬盘和内存。
Memory Cache
Memory Cache 也就是内存中的缓存,读取内存中的数据肯定比磁盘快。主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。(一个tab页面就是一个进程)
当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存
内存缓存不可能存放所有资源,计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。一般主要为脚本、HTML、CSS、图片等等。
实践
运行上面的代码启动 node 服务。再刷新页面,如下所示。
其实这里我是有一个疑问的,因为很多文章都谈到,memory cache 不关心表头的 Cache-Control配置,但是我如果去掉Cache-Contorl或者 改成 max-age=0,no-cache, 根本不会对资源进行内存缓存,希望知道这个问题的大佬可以慷慨解囊!!感激不尽!!
Disk Cache
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
Disk Cache 会严格根据 HTTP 头信息中的各类字段来判定哪些资源可以缓存,哪些资源不可以缓存;哪些资源是仍然可用的,哪些资源是过时需要重新请求的。当命中缓存之后,浏览器会从硬盘中读取资源,虽然比起从内存中读取慢了一些,但比起网络请求还是快了不少的。绝大部分的缓存都来自 Disk Cache。
Disk Cache 是持久的
实践
关闭刚才的Tab页面,重新输入URL, 进入。
缓存策略
通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。
两者的流程如图所示:
强缓存
强制缓存,顾名思义,就是当客户端请求后,会先去找缓存数据库看缓存是否存在。如果存在则强制拿缓存的数据;不存在就再请求,拿过来之后再写入缓存数据库。简单粗暴,我喜欢。
强制缓可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control 。强缓存表示在缓存期间不需要请求,state code 为 200。
Expires
Expires: Wed, 22 Oct 2018 08:41:00 GMT
Expires表示过期时间,资源会在这个时间后过期,需要重新请求。
但是这个东西是有缺点的:
- 这玩意受限于本地时间,如果某个人犯贱修改了时间就会导致缓存失败。而且同时也会被时差或者误差等因素也造成客户端与服务端的时间不一致,致使缓存失效。
- 格式太麻烦,看着头疼
Cache-control
Cache-control: max-age=30
为了补救,在HTTP/1.1中,增加了一个字段Cache-control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求。比如,上面的例子表示该属性值表示资源会在 30 秒后过期,需要再次请求。Cache-control 优先级高于 Expires
常用的 Cache-Control常用的值如下所示:
public: 所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)private: 所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。max-age=30: 资源在30秒后就会失效must-revalidate: 如果超过了max-age的时间,浏览器必须向服务器发送请求,验证资源是否还有效。no-cache:虽然字面意思是“不要缓存”,但实际上还是客户端缓存内容,会立即失效,下次请求时判断是否过期no-store: 真~不要缓存
这些值可以组合起来使用, 比如我想设置过期时间,又能决定在客户端服务端缓存。优先级如下所示
看不懂英文的请自扇巴掌。
协商缓存
上面讲到如果缓存过期了,就需要发起请求验证资源是否有更新。这时候协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag。
当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。
协商缓存是有两组字段的
Last-Modified & If-Modified-Since
Last-Modified 是浏览器返回的最后跟新时间, 与它配合的 If-Modified-Since 是请头上的传递上次返回的Last-Modified
- 服务器通过
Last-Modified字段告知客户端,资源最后一次被修改的时间,例如
Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
- 浏览器将这个值和内容一起记录在缓存数据库中。
- 下一次请求时,请求头中将上次取到的
Last-Modified的值写入到请求头的If-Modified-Since字段 - 服务器会将
If-Modified-Since的值与Last-Modified字段进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。
实践
server.js改成如下
const http = require("http");
const fs = require("fs");
http
.createServer(function(request, response) {
if (request.url === "/") {
const html = fs.readFileSync("index.html", "utf-8");
response.writeHead(200, {
"Content-Type": "text/html"
});
response.end(html);
}
if (request.url === "/index.js") {
if (request.headers["if-modified-since"] === "123") { // 如果相同,则返回 304
response.writeHead(304, {
"Content-Type": "text/javascript",
"Cache-Control": "max-age=200000, no-cache",
"Last-Modified": "123"
});
const js = fs.readFileSync("demo.js", "utf-8");
response.end(js); // 这里无论返回什么都不改变原来的
} else {
response.writeHead(200, {
"Content-Type": "text/javascript",
"Cache-Control": "max-age=200000, no-cache",
"Last-Modified": "123" //这里应该是日期格式,为了方便随便写的
});
const js = fs.readFileSync("index.js", "utf-8");
response.end(js);
}
}
})
.listen(8888);
console.log("listening to 8888");
运行代码,再刷新页面
但是这个也是有缺点的
- 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。
- 因为
Last-Modified只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源 - 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
Etag & If-None-Match
为了解决上述问题, ETag and If-None-Match 应运而生
Etag 存储的是文件的特殊标识(一般都是 hash 生成的),服务器存储着文件的 Etag 字段。之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 变成了 If-None-Match。服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。
Etag 的优先级高于 Last-Modified
总结
memory cache -> disck cache (强缓存 ? 强缓存 : 协商缓存 ? 304 : 200 ) -> 重新发送请求 -> 存储响应内容
对于一开始的问题,我认为强缓存和协商缓存属于硬盘中的缓存的,只是有些文件开始优先存入内存缓存,但关闭Tab后,最终都是都是存入硬盘中的。(如果不对请大佬指正!!万分感谢!!)