从输入URL到渲染页面 —— 浏览器缓存机制

1,117 阅读10分钟

上一篇文章中介绍了 TCP 协议是如何保证数据完整传输的,以及 TCP 连接过程包括了建立连接、传输数据和断开连接三个阶段。我们还介绍了http的发展历程。 这篇文章我们深入 HTTP 的请求过程,并通过分析一个 HTTP 请求过程中每一步的状态来带你了解完整的 HTTP 请求过程。

客户端发起请求

1.构建请求

首先,浏览器会构建类似下面这样的请求行信息,构建好后,浏览器准备发起网络请求。

GET /index.html HTTP1.1

2.查找缓存

在发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件(浏览器缓存是在本地保存资源副本)。 当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。如果缓存查找失败,就会进入网络请求过程了。

3.DNS域名解析

上文我们知道,HTTP 的内容是从 TCP 传输层的传输阶段来传递的,并且数据包通过 IP 地址传输给接收方的。那么我们现在在浏览器中输入的URL可不是IP,浏览器是怎么知道他的IP呢。这就用到了DNS(Domain Name System)技术。—— 负责把域名和 IP 地址做一一映射关系。 浏览器还提供了 DNS 数据缓存服务,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求。 这样知道 IP 之后,接下来就需要获取端口号了。通常情况下,如果 URL 没有特别指明端口号,那么 HTTP 协议默认是 80 端口。

4.建立TCP连接

具体的三次握手建立TCP连接请参考上文

5.发送HTTP请求

浏览器通过发送请求行,请求头和请求体的方式来告诉服务器我们的“诉求”。

服务端返回请求

1.返回请求

服务端通过发送响应行、响应头和响应体的方式来相应浏览器的“诉求”

2.断开连接

通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接(四次挥手)。不过如果浏览器或者服务器在其头信息中加入了:Connection:Keep-Alive(http/1.1)或者http2.0时代。那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。

3.重定向

通常情况服务端的工作在上一步就“结束了”,不过还有一个特殊情况,就是服务端返回状态码301或者302,表示我们请求的页面需要进行重定向。这就是为什么我们可能请求一个网址http://www.test.com 但是最终打开的是http://www.test-redirect.com。

浏览器缓存

上面铺垫了这么多的内容,我们可以看到有两个地方是用到了缓存技术 —— DNS缓存和页面资源缓存 其中,DNS 缓存比较简单,它主要就是在浏览器本地把对应的 IP 和域名关联起来,这里就不做过多分析了,下面是一个简单的 DNS 查询的过程。

浏览器缓存->系统缓存->本地host文件(容易被恶意窜开,所以尽量设置为只读)-> DNS系统调用请求本地域名服务器localDNS(LDNS)来解析域名(只到这里80%以上的情况都能找到了,也是浏览器能做的所有事情了)->后续

下面我们重点说下浏览器的资源缓存。

强缓存

不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的Network选项中可以看到该请求返回200的状态码,并且Size显示from disk cache或from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。

Expires

缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。

Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-Control

Cache-Control是HTTP/1.1的产物。比如如下设置

Cache-Control:max-age=300

则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。

关于cache-control的更多选项,参考MDN的文档https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control

值得一提的是,有两个选项我们经常会弄混,我们说明下: no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control的缓存控制方式做前置验证,而是使用 Etag 或者Last-Modified字段来控制缓存。需要注意的是,no-cache这个名字有一点误导。设置了no-cache之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。

no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存

两者对比

其实这两者差别不大,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况

  • 协商缓存生效,返回304和Not Modified
  • 协商缓存失效,返回200和请求结果

协商缓存同样可以通过设置两种HTTP header实现Last-Modified 和 ETag

Last-Modified和If-Modified-Since

浏览器在第一次访问资源时,服务器返回资源的同时,在response header中添加 Last-Modified的header,表示这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header;

Last-Modified: Sun, 31 May 2020 05:28:04 GMT

浏览器下一次请求这个资源,浏览器检测到有 Last-Modified这个header,于是添加If-Modified-Since这个header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200状态码

但是 Last-Modified 存在一些弊端:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
  • 因为 Last-Modified 只能以秒计时,如果在1s时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源

既然根据文件修改时间来决定是否缓存有缺陷,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP / 1.1 出现了 ETagIf-None-Match

ETag和If-None-Match

流程类似上面。Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接返回200,将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304,客户端直接使用本地缓存即可。

两者对比

  • 同样Last-Modified是HTTP/1.0的产物,而Etag是HTTP/1.1的产物

  • 在精确度上,Etag要优于Last-Modified。

Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。

  • 在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
  • 在优先级上,服务器校验优先考虑Etag

缓存机制

了解了强缓存和协商缓存后,我们来总结下缓存机制。 强制缓存优先于协商缓存,若强制缓存生效则直接使用缓存,若不生效则进行协商缓存,协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。具体流程图如下:

用户行为对浏览器缓存的影响

这里我们分析下用户在操作浏览器时,会触发怎样的缓存策略。主要有 3 种:

  1. 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  2. 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
  3. 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

最后,涉及到浏览器缓存还有两个Service-work和Push Cache,在这里我们就不多介绍了,有兴趣的同学可以自行去翻阅MDN的文档