HTTP缓存
目的
浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。
简单释义
浏览器缓存其实就是浏览器保存通过HTTP获取的所有资源,是浏览器将网络资源存储在本地的一种行为。浏览器的缓存机制是根据HTTP报文的缓存标识进行的。
不同种类的缓存
缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。这样带来的好处有:缓解服务器端压力,提升性能(获取资源的耗时更短了)。对于网站来说,缓存是达到高性能的重要组成部分。缓存需要合理配置,因为并不是所有资源都是永久不变的:重要的是对一个资源的缓存应截止到其下一次发生改变(即不能缓存过期的资源)。
缓存的种类有很多,其大致可归为两类:私有与共享缓存。共享缓存存储的响应能够被多个用户使用。私有缓存只能用于单独用户。本文将主要介绍浏览器与代理缓存,除此之外还有网关缓存、CDN、反向代理缓存和负载均衡器等部署在服务器上的缓存方式,为站点和 web 应用提供更好的稳定性、性能和扩展性。
(私有)浏览器缓存
私有缓存只能用于单独用户
。你可能已经见过浏览器设置中的“缓存”选项。浏览器缓存拥有用户通过 HTTP 下载的所有文档。这些缓存为浏览过的文档提供向后/向前导航,保存网页,查看源码等功能,可以避免再次向服务器发起多余的请求。它同样可以提供缓存内容的离线浏览。
(共享)代理缓存
共享缓存可以被多个用户使用
。例如,ISP 或你所在的公司可能会架设一个 web 代理来作为本地网络基础的一部分提供给用户。这样热门的资源就会被重复使用,减少网络拥堵与延迟。
缓存控制
Cache-control
头
HTTP/1.1定义的 [Cache-Control](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control)
头用来区分对缓存机制的支持情况, 请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。
没有缓存
缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。
Cache-Control: no-store
缓存但重新验证
如下头部定义,此方式下,每次有请求发出时,缓存会将此请求发到服务器(译者注:该请求应该会带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过期,若未过期(注:实际就是返回304),则缓存才使用本地缓存副本。
Cache-Control: no-cache
私有和公共缓存
"public" 指令表示该响应可以被任何中间人(译者注:比如中间代理、CDN等)缓存。若指定了"public",则一些通常不被中间人缓存的页面(译者注:因为默认是private)(比如 带有HTTP验证信息(帐号密码)的页面 或 某些特定状态码的页面),将会被其缓存。
而 "private" 则表示该响应是专用于某单个用户的,中间人不能缓存此响应,该响应只能应用于浏览器私有缓存中。
Cache-Control: private
Cache-Control: public
过期
过期机制中,最重要的指令是 "max-age=<seconds>
",表示资源能够被缓存(保持新鲜)的最大时间。相对Expires而言,max-age是距离请求发起的时间的秒数。针对应用中那些不会改变的文件,通常可以手动设置一定的时长以保证缓存有效,例如图片、css、js等静态资源。
详情看下文关于缓存有效性
的内容。
Cache-Control: max-age=31536000
验证方式
当使用了 "must-revalidate
" 指令,那就意味着缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,已过期的缓存将不被使用。详情看下文关于缓存校验
的内容。
Cache-Control: must-revalidate
Pragma
[Pragma](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Pragma)
是HTTP/1.0标准中定义的一个header属性,请求中包含Pragma的效果跟在头信息中定义Cache-Control: no-cache相同,但是HTTP的响应头没有明确定义这个属性,所以它不能拿来完全替代HTTP/1.1中定义的Cache-control头。通常定义Pragma以向后兼容基于HTTP/1.0的客户端。
新鲜度
理论上来讲,当一个资源被缓存存储后,该资源应该可以被永久存储在缓存中。由于缓存只有有限的空间用于存储资源副本,所以缓存会定期地将一些副本删除,这个过程叫做缓存驱逐。另一方面,当服务器上面的资源进行了更新,那么缓存中的对应资源也应该被更新,由于HTTP是C/S模式的协议,服务器更新一个资源时,不可能直接通知客户端更新缓存,所以双方必须为该资源约定一个过期时间,在该过期时间之前,该资源(缓存副本)就是新鲜的,当过了过期时间后,该资源(缓存副本)则变为陈旧的_。_驱逐算法用于将陈旧的资源(缓存副本)替换为新鲜的,注意,一个陈旧的资源(缓存副本)是不会直接被清除或忽略的,当客户端发起一个请求时,缓存检索到已有一个对应的陈旧资源(缓存副本),则缓存会先将此请求附加一个If-None-Match
头,然后发给目标服务器,以此来检查该资源副本是否是依然还是算新鲜的,若服务器返回了 [304](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304)
(Not Modified)(该响应不会有带有实体信息),则表示此资源副本是新鲜的,这样一来,可以节省一些带宽。若服务器通过 If-None-Match 或 If-Modified-Since判断后发现已过期,那么会带有该资源的实体内容返回。
下面是上述缓存处理过程的一个图示:
对于含有特定头信息的请求,会去计算缓存寿命。比如Cache-control: max-age=N
的头,相应的缓存的寿命就是N
。通常情况下,对于不含这个属性的请求则会去查看是否包含Expires
属性,通过比较Expires的值和头里面Date
属性的值来判断是否缓存还有效。如果max-age和expires属性都没有,找找头里的Last-Modified
信息。如果有,缓存的寿命就等于头里面Date的值减去Last-Modified的值除以10(注:根据rfc2626其实也就是乘以10%)。
缓存失效时间计算公式如下:
expirationTime = responseTime + freshnessLifetime - currentAge
上式中,responseTime
表示浏览器接收到此响应的那个时间点。
改进资源
我们使用缓存的资源越多,网站的响应能力和性能就会越好。为了优化缓存,过期时间设置得尽量长是一种很好的策略。对于定期或者频繁更新的资源,这么做是比较稳妥的,但是对于那些长期不更新的资源会有点问题。这些固定的资源在一定时间内受益于这种长期保持的缓存策略,但一旦要更新就会很困难。特指网页上引入的一些js/css文件,当它们变动时需要尽快更新线上资源。
web开发者发明了一种被 Steve Souders 称之为 revving
的技术[1] 。不频繁更新的文件会使用特定的命名方式:在URL后面(通常是文件名后面)会加上版本号。加上版本号后的资源就被视作一个完全新的独立的资源,同时拥有一年甚至更长的缓存过期时长。但是这么做也存在一个弊端,所有引用这个资源的地方都需要更新链接。web开发者们通常会采用自动化构建工具在实际工作中完成这些琐碎的工作。当低频更新的资源(js/css)变动了,只用在高频变动的资源文件(html)里做入口的改动。
这种方法还有一个好处:同时更新两个缓存资源不会造成部分缓存先更新而引起新旧文件内容不一致。对于互相有依赖关系的css和js文件,避免这种不一致性是非常重要的。
加在加速文件后面的版本号不一定是一个正式的版本号字符串,如1.1.3这样或者其他固定自增的版本数。它可以是任何防止缓存碰撞的标记例如hash或者时间戳。
ETags
作为缓存的一种强校验器,ETag
响应头是一个对用户代理(User Agent, 下面简称UA)不透明(译者注:UA 无需理解,只需要按规定使用即可)的值。对于像浏览器这样的HTTP UA,不知道ETag代表什么,不能预测它的值是多少。如果资源请求的响应头里含有ETag, 客户端可以在后续的请求的头中带上If-None-Match
头来验证缓存。
Last-Modified
响应头可以作为一种弱校验器。说它弱是因为它只能精确到一秒。如果响应头里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since
来验证缓存。
当向服务端发起缓存校验的请求时,服务端会返回 200 ok表示返回正常的结果或者 304 Not Modified(不返回body)
表示浏览器可以使用本地缓存文件。304的响应头也可以同时更新缓存文档的过期时间。
Vary 响应
Vary HTTP
响应头决定了对于后续的请求头,如何判断是请求一个新的资源还是使用缓存的文件。
当缓存服务器收到一个请求,只有当前的请求和原始(缓存)的请求头跟缓存的响应头里的Vary都匹配,才能使用缓存的响应。
按强缓存与协商缓存分类
强缓存 vs 协商缓存
特性 | 强缓存 | 协商缓存 |
---|---|---|
是否发送请求 | 否(直接从本地缓存加载) | 是(向服务器发送请求验证资源是否更新) |
响应状态码 | 200 (from cache) | 304 (Not Modified) |
主要响应头 | Cache-Control 、Expires | Last-Modified 、ETag |
主要请求头 | 无 | If-Modified-Since 、If-None-Match |
适用场景 | 静态资源(如 JS、CSS、图片) | 动态资源(如 HTML 文件) |
一、强缓存(Expires,cache-control )
强缓存是设置过期时间,超过设置的时间会发请求,否则就会走缓存,连请求也不会发。状态码是200
。
Expires
response header里的过期时间,浏览器再次加载资源时,如果在这个过期时间内,则命中强制缓存。
Cache-Control
当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。
Cache-Control 是 http1.1 时出现的 header 信息,主要是利用该字段的 max-age 值来进行判断,它是一个相对时间,例如 Cache-Control:max-age=3600,代表着资源的有效期是 3600 秒。cache-control 除了该字段外,还有下面几个比较常用的设置值:
no-cache
:需要进行协商缓存,发送请求到服务器确认是否使用缓存。
no-store
:禁止使用缓存,每一次都要重新请求数据。
public
:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。
private
:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。
Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高。
Expires和Cache-Control的区别
- Expires 是http1.0的产物,Cache-Control是http1.1的产物
- 两者同时存在的话,Cache-Control优先级高于Expires;
- 在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法
- Expires是一个具体的服务器时间,这就导致一个问题,如果客户端时间和服务器时间相差较大,缓存命中与否就不是开发者所期望的。Cache-Control是一个时间段,控制就比较容易
强缓存的工作流程
- 浏览器首次请求资源时,服务器返回资源并设置
Cache-Control
或Expires
。 - 在缓存有效期内,浏览器直接从本地缓存加载资源,不会向服务器发送请求。
- 缓存过期后,浏览器会向服务器发送请求,进入协商缓存阶段。
强缓存的适用场景
- 静态资源(如 JS、CSS、图片)可以设置较长的缓存时间(如 1 年),并使用哈希文件名确保内容更新后缓存失效。
二、协商缓存(Last-modified ,Etag)
协商缓存常用模式有两种,一种是last-modified
,一种是ETag
。也有地方叫对比缓存的。
last-modified
就是资源的修改时间。如果客户端来请求服务器一个文件,服务器便会给请求头加上last-modified
返回给客户端,客户端下次请求同一个文件时,会带上If-Modified-Since
,这样会对比跟服务端的修改时间是否一样,如果一样说明没改,从浏览器缓存里拿文件,如果不一样说明改了,会去服务端拿文件。ETag
是根据内容来判断文件修改没有。可以发现上面修改时间有几个明显弊端:如果资源是cdn给的,用户第一次拿一个cdn的,第二次拿另一个cdn的文件,实际文件没变,就浪费流量了。另外如果一个文件高速修改,比如1秒钟修改了好几次,实际最后修改时间跟客户端拿的修改时间一样,客户端也会认为文件没改,实际文件已经改了。这个etag就是解决此类问题。- 总结下
last-modified
就是看文件修改时间,ETag
就是看摘要算法,所以ETag
要耗点性能。状态码304
ETag和If-None-Match
这两个要一起说。Etag
是上一次加载资源时,服务器返回的response header,是对该资源的一种唯一标识,只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag
值放到request header里的If-None-Match
里,服务器接受到If-None-Match
的值后,会拿来跟该资源文件的Etag值做比较,如果相同,则表示资源文件没有发生改变,命中协商缓存。
Last-Modified和If-Modified-Since
这两个也要一起说。Last-Modified
是该资源文件最后一次更改时间,服务器会在response header里返回,同时浏览器会将这个值保存起来,在下一次发送请求时,放到request header里的If-Modified-Since
里,服务器在接收到后也会做比对,如果相同则命中协商缓存。
ETag和Last-Modified区别
- 在方式上,
Etag
是对资源的一种唯一标识,而Last-Modified
是该资源文件最后一次更改时间 - 在精确度上,
Etag
要优于Last-Modified
。Last-Modified
的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag
每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified
也有可能不一致。 - 在性能上,
Etag
要逊于Last-Modified
,毕竟Last-Modified
只需要记录时间,而Etag
需要服务器通过算法来计算出一个hash值。 - 在优先级上,服务器校验优先考虑
Etag
。
协商缓存的工作流程
- 浏览器首次请求资源时,服务器返回资源并设置
Last-Modified
或ETag
。 - 浏览器再次请求资源时,会带上
If-Modified-Since
或If-None-Match
。 - 服务器根据请求头判断资源是否更新:
- 如果资源未更新,返回
304 Not Modified
,浏览器从本地缓存加载资源。 - 如果资源已更新,返回新资源并更新缓存。
- 如果资源未更新,返回
协商缓存的适用场景
- 动态资源(如 HTML 文件)或频繁更新的资源,适合使用协商缓存。
设置
强缓存
强缓存设置靠请求头的Cache-Control
或者Expires
,听说有兼容性问题,老的是Expires
,现在越来越多浏览器支持Cache-Control
了,建议全都设上。Cache-Control
设置的是个相对于现在的时间,单位是秒,Expires
设置的是GMT时间,以设置10秒为例:
res.setHeader('Expires',new Date(Date.now()+10*1000).toGMTString())
res.setHeader('Cache-Control','max-age=10')
另外Cache-Control设置no-cache就不会强缓存,no-store不会缓存。
const Koa = require('koa');
const fs = require('fs');
const cors = require("koa-cors")
const app = new Koa();
let a = 10;
// 处理跨域
app.use(cors())
app.use(async (ctx, next) => {
//console.log(ctx)
// 强缓存过期时间
ctx.set('Expires', new Date(Date.now() + 10 * 1000).toGMTString())
ctx.set('Cache-Control', 'max-age=10000')
if (ctx.path === "") {
ctx.body = "Index"
return
}
a++;
if (ctx.path === '/api/jsonp') {
const { cb, msg } = ctx.query;
const body = {
meta: {
status: 20,
message: '请求成功',
a
}
}
ctx.body = JSON.stringify({ ...body })
return;
}
})
const port = 8080
app.listen(port, () => {
console.log("http:127.0.0.1:" + port)
});
const Koa = require('koa');
const path = require('path');
const cors = require("koa-cors")
//静态资源中间件
const resource = require('koa-static');
const app = new Koa();
const host = 'http://127.0.0.1';
const port = 4396;
app.use(cors())
app.use(async (ctx, next) => {
// 设置响应头Cache-Control 设置资源有效期为300秒
ctx.set({
'Cache-Control': 'max-age=300'
});
await next();
});
app.use(resource(path.join(__dirname, './static')));
app.listen(port, () => {
console.log(`server is listen in ${host}:${port}`);
});
我们刷新页面可以看到响应头的Cache-Control变成了max-age=300。
我们顺便来验证下三级缓存原理
我们刚进行了网络请求,浏览器把web.png存进了磁盘和内存中。
根据三级缓存原理,我们会先在内存中找资源,我们来刷新页面。
ok,接下来,我们关掉该页面,再重新打开。因为内存是存在进程中的,所以关闭该页面,内存中的资源也被释放掉了,磁盘中的资源是永久性的,所以还存在。
根据三级缓存原理,如果在内存中没找到资源,便会去磁盘中寻找!
from disk cache !!! ok
,以上也就验证了三级缓存原理,相信你对缓存资源的存储也有了更深的理解了。
我们刚对资源设置的有效期是300秒,我们接下来来验证缓存是否失效。
300秒后。
我们通过返回值可以看到,缓存失效了。
通过以上实践,你是否对强缓存有了更深入的理解了呢?
协商缓存
- 先看Last-Modified设置:
服务端设置Last-Modified后,浏览器请求同一个资源会带if-modified-since的请求头,所以服务端还要获取这个请求头的时间进行对比,相等返回304提前关闭请求即可,浏览器会自己去缓存里拿。
由于Cache-Control
的默认值就是no-cache(需要进行协商缓存,发送请求到服务器确认是否使用缓存。)
,所以我们这里不用对Cache-Control进行设置!
ETag
let statObj= await fs.stat(absPath)
let ctime = statObj.ctime.toGMTString()
res.setHeader('Last-Modified',ctime)//设置
let ifModifiedSince =req.headers['if-modified-since']//获取
if(ifModifiedSince===ctime){
res.statusCode=304
return res.end()//直接返回
}
const express = require('express');
const app = express();
//设置允许跨域访问该服务.
app.all('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', '*');
res.header('Content-Type', 'application/json;charset=utf-8');
next();
});
let a = 10;
app.get('/api/jsonp', (req, res) => {
const ctime = 60 * 1000;
res.setHeader('Last-Modified', 60 * 1000)//设置
let ifModifiedSince = req.headers['if-modified-since']//获取
a++;
if (ifModifiedSince == ctime) {
console.log(ifModifiedSince, a)
res.statusCode = 304
return res.end()//直接返回
} else {
res.send(JSON.stringify({
a
}));
}
});
const port = 8010
app.listen(port, () => {
console.log("http:127.0.0.1:" + port)
});
ETag
ETag
设置要稍微复杂点,主要看摘要算法,一般的摘要算法是用md5,由于md5会暴力破解,所以也可以采用sha1或者sha256等加盐算法,其中盐值就是相当于自己的一个密码本,要暴力破解必须知道盐值才行。
这些算法在nodejs自带的crypto里都有。
//ETag support for Koa responses using etag.
npm install koa-tag -D
// etag works together with conditional-get
npm install koa-conditional-get -D
const Koa = require('koa');
const path = require('path');
//静态资源中间件
const resource = require('koa-static');
const conditional = require('koa-conditional-get');
const etag = require('koa-etag');
const app = new Koa();
const host = 'http://127.0.0.1';
const port = 4396;
// etag works together with conditional-get
app.use(conditional());
app.use(etag());
app.use(resource(path.join(__dirname, './static')));
app.listen(port, () => {
console.log(`server is listen in ${host}:${port}`);
});
我们这里直接使用现成的插件帮我们计算文件的ETag
值,站在巨人的肩膀上!
ok。第一次请求.
我们发现返回值里面已经有了Etag值。
接下来再请求的时候,浏览器将会带上If-None-Match
请求头,并赋值为上一次返回头的Etag
值,然后与 这次返回值的Etag值进行对比。如果一致则命中协商缓存。返回304 Not Modified
。接下来我们来验证一下~
ok,如图所示,完美验证了上面的说法。
接下来我们修改web.png ,来验证是否资源改变时 协商缓存策略也就失效呢?
如图所示.协商缓存的实践也验证了原理。
浏览器缓存过程
- 浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;
- 下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求;
- 服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;
- 如果服务器收到的请求没有Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;
浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效?
判断是否命中强制缓存:当命中强制缓存时,状态码为200, 请求对应的Size值则代表该缓存存放的位置,分别为from memory cache
和 from disk cache
。
from memory cache代表使用内存中的缓存,from disk cache则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为memory
>disk
。
- 内存缓存(from memory cache):内存缓存具有两个特点,分别是
快速读取
和时效性
:
快速读取
:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。
时效性
:一旦该进程关闭,则该进程的内存则会清空。
- 硬盘缓存(from disk cache):硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行I/O操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。退出进程不会清空。
一般JS,字体,图片等会放在内存中,而CSS则会放在硬盘缓存中
为什么CSS会放在硬盘缓存中?
因为CSS文件加载一次就可渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中,但是js之类的脚本却随时可能会执行,如果脚本在磁盘当中,我们在执行脚本的时候需要从磁盘取到内存中来,这样IO开销就很大了,有可能导致浏览器失去响应。
三级缓存原理 (访问缓存优先级)
- 先在内存中查找,如果有,直接加载。
- 如果内存中不存在,则在硬盘中查找,如果有直接加载。
- 如果硬盘中也没有,那么就进行网络请求。
- 请求获取的资源缓存到硬盘和内存。