什么是web缓存
web缓存主要指两个部分:浏览器缓存和http缓存。
浏览器缓存
浏览器缓存相对简单易懂。主要分为localstorage、sessionStorage、cookie等,这些主要是用来存放一些必要的数据,比如用户信息,需要携带到后端的参数等。
不过需要注意:像localstorage、sessionStorage这种保存用户缓存数据的功能,它只能保存5M左右,cookie则更少,大概只有4kb左右的数据。
本篇文章的重点主要是在前端http缓存。
http缓存
根据官方文档描述:HTTP缓存的目标是通过重用先前的响应消息来满足当前请求,从而显著提高性能。
注意:响应消息覆盖面比较广,我们的缓存主要是针对html、css、img等响应消息,常规情况下,我们不会对数据等动态资源进行存储,因为数据等动态资源需要时效性。
http缓存可以解决什么问题?以及有何缺点?
先说说可以解决哪些问题:
- 减少不必要的网络传输
- 更快的加载页面
- 减少服务器负载
缺点:
- 占用内存(重用响应消息,会使这些消息存在内存中)
其实日常的开发中,我们更关心的是页面的快速响应,综合优缺点来说我们还是会使用http缓存策略
详解http缓存
http缓存主要分为强制缓存和协商缓存。
强制缓存
如上图所示,从强制缓存的角度触发,如果浏览器请求的目标资源有效命中强缓存,则可以直接从内存中读取资源,无需与服务器做通信。
基于Expires 字段实现强缓存
Expires:中文含义过期,它的作用就是设定一个强缓存的时间,在此时间内,则从内存或者磁盘中读取缓存返回。
如图所示的百度的图片资源:Expires设置为Sat, 26 Nov 2033 09:27:24 GMT,表示这段时间内都不会去服务器读取资源。都读取本地缓存资源。
但是Expires有严重的缺点
因为Expires判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的Expires中的时间字段做比较,判断是否向服务器发起请求。这里有一个巨大的漏洞:本地时间不准会怎样?
Expires过渡依赖时间戳。如果本地时间和服务器时间不同步,则资源没办法被强制缓存下来。
应该怎么解决这个问题呢?
基于Cache-control实现强制缓存
Cache-control:该字段是http1.1中被增加,它完美的解决了Expires本地时间和服务器时间不同步的问题。
如上图所示:max-age=315360000,表示在315360000秒内资源都从内存或磁盘中读取资源。
Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。
Cache-control有max-age、s-maxage、no-cache、no-store、private、public这六个属性。
- max-age决定客户端资源被缓存多久。
- s-maxage决定代理服务器缓存的时长。
- no-cache表示强制进行协商缓存。
- no-store表示禁止任何缓存策略。
- public表示资源即可以被浏览器缓存也可以被代理服务器缓存。
- private表示资源只能被浏览器缓存。
强制缓存的实现就是以上两种方式了。在日常工作中一般会向上面百度图片一样两种方式一起使用,虽然Cache-control可以很好的替代Expires,但是如果要考虑向下兼容Cache-control不支持的时候还是要使用Expires。
协商缓存
协商缓存相对强制缓存会麻烦一些,需要仔细理解。
基于last-modified的协商缓存
基于last-modified的协商缓存实现方式是:
- 首先需要在服务器端读出文件修改时间
- 将读出来的修改时间赋给响应头的
last-modified字段 - 最后设置
Cache-control:no-cache三步缺一不可。
如图:
- 第一次请求,需要设置响应字段
const {time}= fs.statSync("./test.png")
res.setHeader("last-modufied",time) //以后客服端每一次请求都会携带一个`If-Modified-Since`记录time
res.setHeader("Catch-Control","no-cache")
- 上一步中第一次请求设置了协议字段,浏览器会缓存资源,但是能不能使用缓存资源,服务还需要一段判断逻辑
//当客户端读取到`last-modified`的时候,会在下次的请求标头中携带一个字段:`If-Modified-Since`
const ifModifiedSince = req.header["if-modified-since"]
if (ifModifiedSince === time.toUTCString()){
res.statusCode = 304;
res.end();
return
}
- 浏览器读取到304状态码,协商缓存生效,使用资源
使用以上方式的协商缓存已经存在两个非常明显的漏洞。
1. 因为是根据文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。
2. 当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。
为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)
基于ETag的协商缓存
ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。
文件指纹:根据文件内容计算出的唯一hash值,文件内容一旦改变则hash值也会改变
从校验流程上来说,协商缓存的修改时间比对和文件指纹比对,几乎是一样的。
ETag也有缺点
- ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。
- ETag有强验证和弱验证,所谓将强验证,ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。ETag还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。
值得注意的一点是,不同于cache-control是expires的完全替代方案(说人话:能用cache-control就不要用expiress)。ETag并不是last-modified的完全替代方案。而是last-modified的补充方案(说人话:项目中到底是用ETag还是last-modified完全取决于业务场景,这两个没有谁更好谁更坏)。
在前端开发中一般哪些问对应哪些存储呢?
- 文件命名有hash值的使用强制缓存。
- 没有hash值的使用协商缓存。
因为有hash值的文件只要在开发中一经变更,就生成新的hash值,hash值变更了,我们在前端资源加载中请求地址其实就发生改变了,就算强缓存1万年,也不会生效,因为访问的资源都是最新的。
而没有hash值的文件,如果对文件修改了,文件名称并没有改变,前端资源请求还是同一资源,此时我们如果设置强制缓存,那么在强制缓存生效时间内都不会与服务器发生交互,从未会导致客服端资源与服务器不一致问题。
总结一下
- http缓存可以减少宽带流量,加快响应速度。
- 关于强缓存,
cache-control是Expires的完全替代方案,在可以使用cache-control的情况下不要使用expires - 关于协商缓存,
etag并不是last-modified的完全替代方案,而是补充方案,具体用哪一个,取决于业务场景。 - 有些缓存是从磁盘读取,有些缓存是从内存读取,有什么区别?答:从内存读取的缓存更快。
- 所有带304的资源都是协商缓存,所有标注(从内存中读取/从磁盘中读取)的资源都是强缓存。