HTTP缓存机制

256 阅读7分钟

缓存机制

HTTP缓存分为强缓存与协商缓存,而HTTP1.1版本对于1.0版本的缓存缺陷做了一些升级与优化。

缓存的目的是为了提高速度并减少流量,现在我们假设有文件A、文件B和文件C三个大小均为10K的文件,并且每个HTTP请求与响应头大小均为1K,在没有任何的缓存机制的情况下,我们看看会发生什么。

第一次请求,浏览器向服务器发送三个HTTP请求分别请求文件A、B和C,消耗了三个HTTP请求一共3K的流量。
服务端收到三个请求并返回三个对应的响应头一共6K,加上浏览器需要的文件A、B和C的响应体一共30K,总计36K流量。
第二次、第三次请求同样是36K,所以最后三次请求一共耗费了108K的流量。

如果有缓存机制并且缓存内容没有过时的情况下将会是这样的。

第一次请求,耗费36K流量,第二次请求直接从缓存中读取,耗费0K(协商缓存只需返回HTTP请求和对应的响应头总计6K),第三次同理,总计耗费36K流量(协商缓存总计36K+6K+6K=48K)

所以在有缓存机制的情况下,将会对除了第一次请求以外的其他请求有很大速度和流量的优化作用,前文中虽然没有举出速度方面的例子,但其实也跟上述流量的例子类似。

强缓存

当浏览器请求资源时,首先会进行强缓存,又可以称为强制缓存,强缓存中的内容保存在浏览器端,所以不需要向服务端发送任何请求。

当强缓存命中时,浏览器会直接拿自己的缓存内容并返回200,此时将不会再进行协商缓存。如果强缓存未命中,浏览器才会进行协商缓存。

由于强缓存始终不会向服务端发送请求,浏览器就没有办法感知到服务端那里真实的文件是否发生了改变或者自己这里的缓存内容是否已经过时了。毕竟真实文件也不在咱这,咱也不知道啊,那么浏览器是如何得知强缓存是否命中的呢?秘密就在HTTP头部。

Expires

Expires是HTTP1.0的产物,浏览器向服务器进行资源请求的时候,服务器会在响应头中加入这个字段并返回一个到期时间。当浏览器再次进行请求前会对比当前时间与Expires来判断资源是否过期,未过期则直接命中强缓存从缓存中读取。

但是这个时间是由服务端生成的,而浏览器的本地时间又是可以随意更改的,一个是浏览器端和服务端的时间存在误差,另一个是当浏览器的本地时间被更改后就会严重影响强缓存的命中判断。

Cache-Control

针对上文中提到的Expires存在的问题,HTTP1.1推出了Cache-Control。同样该字段存于HTTP头部,且优先级高于Expires,只有当该字段不存在时,才使用Expires。

相比于Expires,Cache-Control会更加灵活,它能够设置不进行任何缓存(no-store),只使用协商缓存(no-cache)或者一个指定的可持续时间(max-age)。

max-age以秒为单位,这里可能会有一个疑惑,什么是一个指定的可持续时间,它与Expires的区别是什么,不都是时间么?

我们现在假设max-age的值为60,这意味着这个文件的强缓存命中只能持续一分钟,我服务端并不管你浏览器是从哪个时间点进行计算,又到哪个时间点结束,你自己进行计算处理。反正只要在一分钟以内你就别来烦我(命中强缓存),如果你强行来找我,我也是会给你返回资源的,但是这个过程中产生的流量与速度的消耗我可不负责哈。这样就解决了两端的时间误差问题,所有的时间都以浏览器端的为准。

协商缓存

前文提到强缓存不需要向服务端发送请求,但是协商缓存却始终要向服务端发送请求。如果某个文件在强缓存过期后也一直没有更新过,显然浏览器端应该向缓存中获取资源,但是事实却是又回到了最初的那个问题:我该怎么知道文件是否更新过,所以我们需要协商缓存。

Last-Modified和If-Modified-Since

Last-Modified和If-Modified-Since是HTTP1.0的产物,它们是以资源修改时间为值(以秒为单位,无法感知毫秒级别的资源修改)。与强缓存不同的是,强缓存的命中判断都由浏览器来做,因为浏览器自始至终都不会向服务端发送任何请求,而协商缓存的命中判断都由服务端来做,毕竟真实文件在服务端这里。

第一次请求,浏览器向服务端发送资源请求,服务端返回响应体与响应头,响应头中包含Last-Modified,响应体中包含资源文件,此时浏览器会将Last-Modified与资源进行缓存。
之后当浏览器再向服务端发送资源请求并未命中强缓存时,会在请求头中加入If-Modified-Since,它的值其实就是上一次请求中服务端返回的Last-Modified,由服务端根据资源的最后修改时间来进行比对。如果时间一致就表示资源没更新过,命中协商缓存,只返回304响应头告知浏览器读取缓存中的内容,否则返回200响应头和包含资源文件的响应体。

Etag和If-None-Match

如果根据时间来进行判断也未免太不精确了,假设某文件被修改过但是实际内容根本没有发生变化,照理来说应该判断命中协商缓存,但是资源的修改时间发生了变化导致服务端判断协商缓存未命中,从而浪费了流量与速度。

比如某用户在进行写作,写着写着发现某一行写得有错误就删掉了,然后后来查阅了资料后又发现其实写得是对的,最后把这一行又加上了。这时候其实这篇文章的内容与原先一致并没有发生变化,但是由于该用户修改了两次导致文件的修改时间对不上了。

所以HTTP1.1推出了Etag,服务端会对资源进行一个hash算法后产生一个hash值作为ID并返回浏览器,当浏览器再次进行请求时会将上一次服务端返回的Etag放到If-None-Match中发送给服务端,服务端进行ID的比对来进行判断。

但是hash算法会产生性能上的消耗,当一个文件频繁发生修改时,服务端需要频繁进行比对,如果确实内容都发生了修改就需要频繁生成hash值,这时候可能还不如Last-Modified。所以从性能的角度来说,Last-Modified更胜一筹,从精确的角度来说,Etag能够更加节省流量,各有千秋,不像强缓存那边处于一边倒的态势。

不过综合来说还是Etag更合适,毕竟一般情况下产生的性能消耗是可以忽略不计的,非常频繁修改资源的场景实际情况中非常少见,所以Etag作为HTTP1.1的产物优先级高于Last-Modified。