通过复用以前获取的资源,可以显著提高网站和应用程序的性能。Web 缓存减少了等待时间和网络流量,因此减少了显示资源表示形式所需的时间。通过使用 HTTP 缓存,变得更加响应性。 通常 http 缓存分为强缓存和协商缓存。 在约定的固定时间内,直接使用浏览器本地缓存。这种方式适用于不常更新的资源中,如 js,css,image 等静态资源;同样这也是一个缺点,若缓存时间设置的过长,更新内容不及时;若缓存时间设置的过短,又造成内容没更新,但缓存时间到了的问题。 设置强缓存一般有两种方式: 在服务端设置这个字段,表示该资源的过期时间。 浏览器就会拿其本地时间来跟这个字段中的时间进行对比,若还没到过期时间,则继续使用缓存,否则产生新的请求。 这就会存在一个问题,该资源缓存的长短,与其本地时间有关系,比如设置本地电脑时间在 2039 年,目前所有下发的资源,都无法缓存。 Expires 是 HTTP1.0 时就存在字段,为了解决上面 Expires 存在的问题,在 HTTP1.1 中,引入了 若我们用 nodejs 搭建一个简单的 http 服务的话,设置该字段也很简单。 若使用 当缓存起作用时,会提示该资源 在 Chrome 浏览器中,刷新页面时,会发现 cache-control 或者 Expires 失效了,这是因为: 如果在同一标签中对同一 URI 的另一个请求后立即发出请求(通过单击刷新按钮,或 F5 之类的),Google Chrome 会忽略该标头 Cache-Control 或 Expires 标头。它可能有一个算法来猜测用户真正想做什么。 那怎么才能看到效果呢?新开启一个窗口,打开控制台,然后再请求链接,会发现走缓存了,Is Chrome ignoring Cache-Control: max-age?。 cache-control 的值主要由 3 部分组成,可以单独或组合使用任何部分,(具体可参考该文档Cache-Control): 这几个值可以任意的组合,比如: 一般不太改变的文件,各个环节都可以缓存,缓存时间为 600s(↓): 关闭缓存,任何地方都不存储(↓): 客户端可以缓存资源,但每次都必须重新验证其是否有效(↓),如指定 这两个字段都决定了资源的过期时间,那如果两个同时出现,浏览器要看哪一个呢? 根据RFC2616的定义: If a response includes both an Expires header and a max-age directive, the max-age directive overrides the Expires header, even if the Expires header is more restrictive. This rule allows an origin server to provide, for a given response, a longer expiration time to an HTTP/1.1 (or later) cache than to an HTTP/1.0 cache. This might be useful if certain HTTP/1.0 caches improperly calculate ages or expiration times, perhaps due to desynchronized clocks. 当两者同时存在时,max-age 的优先级更高。 Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。 那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢? 这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。 当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存。 内存缓存中有一块重要的缓存资源是 preloader 相关指令(例如)下载的资源。总所周知 preloader 的相关指令已经是页面优化的常见手段之一,它可以一边解析 js/css 文件,一边网络请求下一个资源。 需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的 HTTP 缓存头 Cache-Control 是什么值,同时资源的匹配也并非仅仅是对 URL 做匹配,还可能会对 Content-Type,CORS 等其他特征做校验。 Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。 在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache,关于 HTTP 的协议头中的缓存字段,我们会在下文进行详细介绍。 浏览器会把哪些文件丢进内存中?哪些丢进硬盘中? 关于这点,网上说法不一,不过以下观点比较靠得住: 强缓存下请求数据的流程: 根据内容最后的修改时间(Last-Modified),或者标识(ETag),来判断内容是否发生了变化,若没有变化,则告诉浏览器直接使用缓存即可,否则返回最新的内容。 协商缓存是每次都要请求服务器的,然后服务器校验是走缓存,还是下发新的内容。当可以使用客户端的缓存时,只需要返回 若上次响应带有 我们以 express 框架为例,来实现下这个缓存逻辑。 这样就能通过资源的修改时间,来决定是否使用缓存。 但用这个字段判断一致性时不太准确,原因如下: 针对这些问题,http 协议中又出现了一个 etag 是对资源内容的标识符,如 hash 值。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web 服务器不需要发送完整的响应。而如果内容发生了变化,使用 ETag 有助于防止资源的同时更新相互覆盖(“空中碰撞”)。 如果给定 URL 中的资源内容更改,则一定要生成新的 Etag 值。 因此 Etags 类似于指纹,也可能被某些服务器用于跟踪。 比较 etags 能快速确定此资源是否变化,但也可能被跟踪服务器永久存留。 若上次响应带有 我们以 express 框架为例,来实现下这个缓存逻辑。 当 etag 与 last-modified 两者同时出现时,etag 的优先级更高;同理,if-none-match 比 if-modified-since 的优先级更高。 同时,etag 对资源更新的控制粒度比 last-modified 也更好,无论源文件怎样修改,只要内容不变,etag 就不会变,则缓存就可以一直使用。 协商缓存下的请求流程: 上面我们讲解了强制缓存与协商缓存的实现机制与实现方式,各位可根据自己资源的缓存特性,来决定使用哪种缓存方式和要缓存的时间。 一般地,强制缓存的优先级比协商缓存的优先级更高,优先判断是否存在强制缓存;1. 强缓存 #
1.1 Expires #
Expires: Wed, 21 Oct 2017 07:28:00 GMT
1.2 cache-control #
cache-control
字段。该字段不是用统一的过期时间来控制,而是告诉浏览器要缓存多长时间,从第一次收到这个请求开始算起。const http = require('http');
http
.createServer((req, res) => {
console.log('get static request:', req.url);
res.writeHead(200, {
'cache-control': 'public, max-age=31536000',
});
res.end('console.log(Date.now())');
})
.listen(3031);
express
的话,可以这样设置:const express = require('express');
const app = express();
app.get('*', (req, res) => {
console.log('get static request:', req.url);
// 下面的3种方式都可以
res.setHeader('cache-control', 'public, max-age=600');
// res.header('cache-control', 'public, max-age=600');
// res.set({ 'cache-control': 'public, max-age=600' });
res.end('console.log(Date.now())');
});
app.listen(3031);
from memory cache
或from disk cache
,服务端也不再接收该请求。
测试 Cache-Control 标题的一种方法是返回带有自身链接的 HTML 文档。点击该链接后,Chrome 会从缓存中投放文档。1.3 cache-control 的其他用法 #
1.3.1 cache-control 的组成部分 #
no-store
比no-cache
更加严格,no-cache 是不走强缓存,但协商缓存还可以使用,但 no-store 是不缓存任何内容。
https://
transactions。1.3.2 使用示例 #
cache-control: public, max-age=600;
cache-control: no-store;
no-cache
或max-age=0
, must-revalidate
等:Cache-Control: no-cache;
Cache-Control: max-age=0, must-revalidate;
1.4 cache-control 与 Expires 的优先级 #
1.5 memory cache 和 disk cache 的区别 #
2. 协商缓存 #
304
状态码即可。2.1 last-modified #
last-modified
是一个响应首部,表示资源最后一次修改的时间,用来判断接收到的或者存储的资源是否彼此一致。2.1.1 使用 last-modified 实现缓存 #
last-modified
字段,则后面请求资源时,会自动在头部带上If-Modified-Since
字段,值就是上次 last-modified 返回的值。我们服务器就可以通过这个If-Modified-Since
字段与资源的修改时间进行对比,若没有变化,直接返回304
即可,若产生变化,则下发新的内容和新的last-modified
字段。const express = require('express');
const fs = require('fs');
const app = express();
let lastModifiedTime = 0;
app.get('*', (req, res) => {
console.log('get static request:', req.url);
// 获取请求头中的 if-modified-since ,若有该字段,且能与上次的修改时间相同
// 则直接返回304
const ifModifiedSince = req.headers['if-modified-since'];
if (ifModifiedSince && new Date(ifModifiedSince).valueOf() === lastModifiedTime) {
res.status(304).end();
return;
}
const file = './app/static/hunger.js';
// 读取文件时,实际上应当使用 createReadStream ,我们使用readFile仅是为了方便演示,
// 因为使用readFile在高并发时,会产生内存积压的问题
// readFile会将所有的内容都读取到内存中,
// 而 createReadStream 则是分片读取,只要读取了内容就会传向下一个管道
fs.readFile(file, { encoding: 'utf8' }, (err, data) => {
if (err) {
return res.status(500).end('read file error');
}
const { mtimeMs } = fs.statSync(file);
// 存储该文件最后的修改时间,并将其设置为响应头进行返回
lastModifiedTime = Math.floor(mtimeMs);
res.setHeader('last-modified', new Date(lastModifiedTime).toGMTString());
res.end(data);
});
});
app.listen(3031);
2.1.2 last-modified 的缺陷 #
etag
的响应字段。2.2 etag #
2.2.1 使用 etag 实现缓存 #
etag
字段,则后面请求资源时,会自动在头部带上If-None-Match
字段,值就是上次 etag 返回的值。我们服务器就可以通过这个If-None-Match
字段与资源的 tag 进行对比,若没有变化,直接返回304
即可,若产生变化,则下发新的内容和新的etag
字段。const express = require('express');
const fs = require('fs');
const crypto = require('crypto');
const md5 = (str) => crypto.createHash('md5').update(str).digest('hex');
const app = express();
let etag = '';
app.get('*', (req, res) => {
console.log('get static request:', req.url);
// 获取请求头中的 if-none-match ,若有该字段,且能与上次的etag相同
// 则直接返回304
const ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch && ifNoneMatch === etag) {
res.status(304).end();
return;
}
const file = './app/static/hunger.js';
// 同理这里实际上我们最好使用 createReadStream
fs.readFile(file, { encoding: 'utf8' }, (err, data) => {
if (err) {
return res.status(500).end('read file error');
}
// 这里我们使用md5来生成etag标识
etag = md5(data);
res.setHeader('etag', etag);
res.end(data);
});
});
app.listen(3031);
2.2.2 etag 与 last-modified #
3. 总结 #
版权属于:
加速器之家
作品采用:
《
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
》许可协议授权
评论