浏览器的强缓存和协商缓存

简介

在前端工程化中,前端代码打包之后的生成的静态资源就要发布到静态服务器上,这时候就要做对这些静态资源做一些运维配置,其中,gzip和设置缓存是必不可少的。这两项是最直接影响到网站性能和用户体验的。

浏览器缓存主要有以下几个优点:

  • 减少重复数据请求,避免通过网络再次加载资源,节省流量。
  • 降低服务器的压力,提升网站性能。
  • 加快客户端加载网页的速度, 提升用户体验。

浏览器缓存分为强缓存和协商缓存,两者有两个比较明显的区别:

  • 强缓存在资源过期之前,不会向服务器发送网络请求,直接从本地缓存读取资源,响应码为200 (from memory cache)或者 200 (from disk cache);
  • 协商缓存会向服务器发送一次请求,服务器会根据这个请求的Request Headers的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的Response Headers通知浏览器从缓存中读取资源;

强缓存

强缓存是根据返回头中的expires(http 1.0 规范)或者Cache-Control(http 1.1 规范)两个字段来控制的,都是表示资源的缓存有效时间。

  • expires的值是一个GMT格式的时间,这个时间代表资源的过期时间,在这个时间之前请求该资源,将直接命中强缓存。但是这个时间有个缺点,它是一个绝对时间,如果本地时间被修改,则将会导致缓存失效,所以expires还是有些缺陷的。

  • 为了解决上述问题,http 1.1协议重新给了一个参数Cache-Control,这个值也是设置资源的过期时间,但是这个参数的值是一个相对时间,比如cache-control: max-age=3600,浏览器会根据这个相对时间结合响应头的date参数,得出资源的过期时间是date的时间加上3600秒,需要注意的是,并不是每次请求都进行计算生效时间,只有当当前请求是200,或者304的时候,才会进行计算,否则每次计算,每次都加3600秒,那资源永远都不会过期了。

    Cache-Control还有一些其他的值可以设置

    1. no-cache:不使用强缓存,直接使用协商缓存。
    2. no-store:直接禁止浏览器缓存数据,每次请求资源都会向服务器要完整的资源, 类似于 network 中的 disabled cache
    3. public:可以被所有用户缓存,包括终端用户和 cdn 等中间件代理服务器。
    4. private:只能被终端用户的浏览器缓存。

如果 Cache-Controlexpires 同时存在的话, Cache-Control 的优先级高于 expires

协商缓存

协商缓存是由服务器来确定缓存资源是否可用。 主要涉及到两对属性字段,都是成对出现的,即第一次请求的响应头带上某个字, last-modified 或者 etag,则后续请求则会带上对应的请求字段 if-modified-since或者 if-none-match,若响应头没有 last-modified 或者 etag 字段,则请求头也不会有对应的字段。

  • last-modified/if-modified-since,值是GMT格式的时间字符串, last-modified 标记最后文件修改时间, 下一次请求时,请求头中会带上的 if-modified-since 值就是 last-modified ,告诉服务器我本地缓存的文件最后修改的时间,在服务器上根据文件的最后修改时间判断资源是否有变化, 如果文件没有变更则返回 304 Not Modified ,请求不会返回资源内容,浏览器直接使用本地缓存。如果有变化,则返回200,返回最新的资源。

  • etag/if-none-match, 值是由服务器为每一个资源生成的唯一标识串,只要资源有变化就这个值就会改变。服务器根据文件本身算出一个唯一值并通过 etag字段返回给浏览器。当下次请求时,浏览器会将上次接收到的etag值赋给 if-none-match 字段,服务器通过比较两者是否一致来判定文件内容是否被改变。

etag的优先级比last-modified要高,先判断资源是否有更新

请求流程

浏览器在第一次请求后缓存资源,再次请求时,会进行下面两个步骤:

  • 浏览器会获取该缓存资源的 header 中的信息,根据 Response Headers 中的 expirescache-control 来判断是否命中强缓存,如果命中则直接从缓存中获取资源。
  • 如果没有命中强缓存,浏览器就会发送请求到服务器,这次请求会带上 IF-Modified-Since 或者 IF-None-Match, 它们的值分别是第一次请求返回 Last-Modified 或者 Etag,由服务器来对比这一对字段来判断是否命中。如果命中,则服务器返回 304 状态码,并且不会返回资源内容,浏览器会直接从缓存获取;否则服务器最终会返回资源的实际内容,并更新 header 中的相关缓存字段。

我们通过几个例子来实际看看会是什么效果,注意,设置缓存可以通过代理服务器设置,也可以直接使用代码设置,这里我们为了方便,直接使用nginx进行设置,nginx设置请求头需要ngx_http_headers_module模块支持,通过yum安装的nginx默认不支持此模块,需要另行编译安装;

下面的例子,我们设置缓存事件都设置为30s,只是为了看效果,实际上不可能设置这么短的时间

设置expires

location ~* \.(js|css)$ {
  expires 30s;
}

第一次请求直接返回200,可以看到虽然我们仅仅设置了expires,但是响应头中也会带上cache-controlexpires是一个绝对时间,该时间由date的时间加上设置的30s

在过期时间内重复请求,都会命中强缓存,直接从浏览器本地读取资源

如果超过了过期时间,则不会命中强缓存,走协商缓存,返回304(因为资源没有发生变化),同时我们可以看到,dateexpires都更新了,重新计算缓存过期时间

设置cache-control: max-age=30

location ~* \.(js|css)$ {
  add_header Cache-Control max-age=30;
}

设置了cache-control,此时可以看到响应头中已经不存在expires了,其他的请求行为和设置expires行为一样,这里不贴图了。

设置cache-control: no-cache,不使用强缓存,使用协商缓存

location ~* \.(js|css)$ {
  add_header Cache-Control no-cache;
}

第一次请求直接返回200,并设置cache-controlno-cache

因为不使用强缓存,直接使用协商缓存,则之后每次都会返回304(因为资源没有发生变化)
注意每次请求都会讲上次响应头里的etag值(通过if-none-match请求头)和last-modified(通过if-modified-since请求头)发送给服务端

使用协商缓存,资源发生了改变(etag会发生变化)

如果不使用强缓存,并且资源没有发生变化,则之后的每次请求都会返回304,但是如果我们的资源发生了变化,此时会将上次etag值(通过if-none-match请求头发送给服务器)和服务器上的最新资源etag值进行比较,发现资源更新了,则返回响应码200,并返回最新的资源。
资源没有发生变化,多次请求返回304,etag值为60ede052-5dd

修改config.js文件,更新资源,此时etag值会发生变化,此时etag值为60ede48c-5eb,响应码为200,请求了最新的资源。

使用协商缓存,资源没有改变,但是文件时间戳更新了(last-modified会发生变化)

我们先将config.js文件还原,避免混淆;
我们先看看当前文件的最后修改时间是多少,我们通过linuxstat命令查看,时间是2021-07-14 03:17:06.109141809 +0800,注意是UTC时间。

多次请求,返回304,注意此时last-modified时间为Tue, 13 Jul 2021 19:17:06 GMTGMT格式,加8个小时,正好是上面的UTC时间

此时我们更新一下时间戳,通过linuxtouch命令,更新config.js文件的时间戳(修改时间也会被修改),此时修改时间为2021-07-14 03:25:30.121617196 +0800

再次请求资源,响应头中的last-modified时间正好是我们更新时间戳后的时间,而上次的last-modified时间通过请求头中的if-modified-since传递给后端(注意GMTUTC时间转换),发现两者时间不一致,响应码为200,请求了最新的资源。

etag拓展

HTTP1.1 中etag的出现主要是为了解决几个last-modified比较难解决的问题:

  • 一些文件也许会周期性的更改,但是内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新请求;
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),if-modified-since能检查到的粒度是秒级的,使用etag就能够保证这种需求下客户端在1秒内能刷新 N 次 cache。
  • 某些服务器不能精确的得到文件的最后修改时间。

etag计算方式

etag计算方式并没有明确规定,只要能标识文件唯一性就行了,但是对于大量的http请求,计算etag不能有太高的消耗,否则对响应速度以及服务器的运算能力都会有影响

etag默认格式是xxxxxxxx-xxx,http默认计算etag的方式是通过文件的last-modifiedcontent-length值的十六进制数标识的,形如${last-modified}-${content-length}

对于以下config.js文件

通过js计算该文件的etag

`${(new Date('Tue, 13 Jul 2021 19:25:30 GMT').getTime() / 1000).toString('16')}-${Number(1501).toString('16')}`

// 60ede8aa-5dd

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com