浏览器缓存原理

万马奔腾
• 阅读 385

本文可以配合本人录制的视频一起食用

目的

通常说到浏览器缓存,大多是和性能优化有关,使用缓存,通常是两个主要目的,第一是提高访问速度,第二是减少网络IO消耗。

当合理配置了缓存,可以得到提升用户体验、减轻服务器负担、节省带宽等效果,这是一种效果显著的前端性能优化手段。

四个方面

浏览器缓存机制涉及四个方面,按照获取资源时请求的优先级排序如下:

  • Memory Cache
  • Service Worker Cache
  • HTTP Cache
  • Push Cache

其中最后一个Push Cache是HTTP2的新特性

缓存策略

通常我们面试时说的浏览器缓存原理,指的是缓存策略。

缓存策略分为两类:强缓存和协商缓存,通过设置HTTP HEADER来实现。

强缓存

强缓存优先级较高,在命中强缓存失败的情况下,才会走协商缓存。

当我们发起请求,去请求一个资源时,浏览器会去读取HTTP Header中的expires或Cache-control,来判断目标资源是否命中强缓存。

使用expires还是Cache-control和我们的HTTP协议版本有关,当使用1.0版本就根据expires进行判断,当使用1.1就根据Cache-control进行判断。

如果命中了强缓存,就直接从缓存中获取资源,不与服务端发生通信。此时返回的HTTP状态码为200,但会有from memory cache的标识。

【判断资源是否过期

浏览器是如何根据expires和Cache-control进行判断的呢?

首先看expires,expires是一个时间戳,指的是资源过期时间,当我们试图再次从服务器请求同一份资源时,浏览器就会先对比本地时间和expires的值,如果本地时间小于expires设置的过期时间,就直接从缓存中获取资源。

因为expires的使用依赖于本地时间,这就会存在问题,不同的本地时间会使缓存的失效时间无法保证,也就是服务端和客户端会存在时差。

因此HTTP 1.1增加了Cache-control作为expires的完全替代方案,现在依然使用expires的唯一目的是为了向下兼容。两者同时使用时,Cache-control的优先级更高。

在Cache-control中我们可以设置max-age来控制资源的有效期,这是一个时间长度,规避了时间戳带来的问题,客户端会记录请求到资源的时间点,以此作为相对时间的起点,确保参与计算的两个时间节点都来源于客户端。

协商缓存

如果强缓存命中失败,浏览器就需要向服务器去询问缓存的相关信息,进而判断是重新请求、下载完整的响应,还是从本地中获取缓存的资源,此时就进入了协商缓存的阶段。

如果我们不想直接使用本地缓存,也可以设置Cache-Control: no-cache直接进入协商缓存的阶段。

协商缓存,从字面上可以理解为:根据客户端和服务器的协商结果,来判断是否使用本地缓存。

如果服务器提示目标资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是304。

【判断资源有效性

那么服务器是如何去判断资源是否变动呢?

有两种判断方式:Last-Modified和Etag

首先看Last-Modified,它是一个时间戳,如果启用了协商缓存,它会在请求时随着response headers返回。

Last-Modified:

之后我们每次请求时,请求头request headers中就会带上一个时间戳字段,叫If-Modified-Since,它的值就是之前response返回的Last-Modified的值。字面上的意思就是在告诉服务器:在这个时间之后,资源如果发生变化,就要返回新的资源

服务器接收到这个字段后,就会与该资源在服务器上的最后修改时间进行比对,两者是否一致,从而判断资源是否发生变化。

  • 如果发生变化

    会返回一个完整的响应。并在响应头response headers中返回新的Last-Modified的值

  • 如果没有发生变化

    就会返回一个状态码为304的响应,提示浏览器使用本地缓存

Last-Modified和If-Modified-Since配合使用,看上去没什么问题,但实际存在一些弊端,比如:

  • 编辑了文件,但文件内容实际没有变化的情况

    我可能修改了一个文件,然后又撤销了修改,虽然文件内容没变,但资源的最后修改时间被更新了。

    导致需要重新将资源返回给客户端,无法充分利用缓存

  • 第二种情况,修改文件的速度过快,导致客户端使用了过期缓存

    因为If-Modified-Since只能检查以秒为最小计量单位的时间差,如果在不可感知的时间内修改了资源,就会使客户端使用过期的资源

为了弥补Last-Modified的不足,就出现了Etag

Etag是服务器为每个资源生成的唯一标识字符串,基于文件内容编码,能够精准感知文件内容变化

首次请求时,客户端会在response headers获得一个标识字符串,类似:

Etag: "dec8d6f46497a9c6b4dff4e237cabe5d"

再次请求时,请求头request headers里会带上一个与Etag值相同的、名为If-None-Match的字段,字面上的意思就是在请求时告诉服务器:资源的Etag如果不一致,就要返回新的资源

If-None-Match的值就是供服务器进行比对:

If-None-Match:  "dec8d6f46497a9c6b4dff4e237cabe5d"

Etag很好的弥补了Last-Modified的短板,但是也存在弊端,因为生成Etag需要服务器付出额外的开销,这会影响服务器的性能,这是启用Etag时需要考虑到的问题。

当Etag和Last-Modified同时存在时,Etag的优先级更高,因为它在感知文件变化上比Last-Modified更准确

如何配置?

了解了缓存策略,那在面对一个具体的缓存需求时,我们该如何配置呢?

浏览器缓存原理

网上这一张应该挺多人看到过的图,我也直接拿过来用,就是根据你项目的需求进行判断

  • 考虑这个响应是否是可复用的,也就是是否使用缓存

    • 如果是No,也就是不可复用,就设置no-store,拒绝一切形式的缓存,包括客户端和服务器的缓存

      Cache-Control: no-store
    • 如果是可复用响应

      • 并且需要每次都进行资源有效性验证,就设置no-cache,会跳过强缓存判断,直接进入协商缓存阶段,向服务器进行资源有效性验证

        Cache-Control: no-cache
      • 考虑是否可被代理服务器缓存

        • 如果只能被浏览器缓存,就设置为private,这也是默认值
        Cache-Control: private
        • 如果即可被浏览器缓存,也能被代理服务器缓存,就设置为pulic
        Cache-Control: public
      • 继续我们可以考虑设置缓存有效时间,以保证缓存的有效性

        • 设置代理服务器缓存的有效时间,使用s-maxage,单位为秒
        Cache-Control: public s-maxage=30
        • 设置浏览器缓存的有效时间,使用max-age,单位也为秒
        Cache-Control: max-age=30

        s-maxage优先级高于max-age,如果s-maxage未过期,则向代理服务器请求其缓存内容,只针对public缓存有效。

      • 最后设置协商缓存需要用到的ETag、Last-Modified等参数

这个图大概就是这么一个流程。

实操

以上大多都是我查到的一些资料,接下来做一些简单的配置,以科学的精神验证一部分内容。

下面的测试在nginx和Chrome中进行。

当不做配置时

location /cache-demo {
    root   html;
    index  index.html;
}
  • 第一次访问页面

    // 响应码 200
    Status Code: 200 OK
    // 响应头里加上了Last-Modified和Etag
    Last-Modified: Thu, 27 Jul 2023 02:47:55 GMT
    ETag: "64c1dadb-e3"
    // 第一次请求头里没有`If-Modified-Since`和`If-None-Match`
  • 再次请求,status变成了304,说明资源内容没有更改

    // 响应码304
    Status Code: 304 Not Modified
    // 响应头里Last-Modified和Etag和之前一次是一样的
    Last-Modified: Thu, 27 Jul 2023 02:47:55 GMT
    ETag: "64c1dadb-e3"
    // 这一次请求头里带上了`If-Modified-Since`和`If-None-Match`
    If-Modified-Since: Thu, 27 Jul 2023 02:47:55 GMT
    If-None-Match: "64c1dadb-e3"
  • 此时我们去修改资源内容,再去重新访问,响应码重新变成了200

    // 响应
    HTTP/1.1 200 OK
    Last-Modified: Thu, 27 Jul 2023 03:50:18 GMT
    ETag: "64c1e97a-e3"
    // 请求头
    If-Modified-Since: Thu, 27 Jul 2023 02:47:55 GMT
    If-None-Match: "64c1dadb-e3"

    可以看到响应头中的Etag和请求头中的If-None-Match不一致,Last-Modified和If-Modified-Since也不一致了,所以就返回了新的资源内容

配置no-store

接下来先做第一种配置,Cache-Control设置为no-store

add_header 'Cache-Control' 'no-store';
  • 第一次访问

    // 响应
    HTTP/1.1 200 OK
    Last-Modified: Thu, 27 Jul 2023 03:50:18 GMT
    ETag: "64c1e97a-e3"
    // 在响应头中看到了no-store的配置
    Cache-Control: no-store
    // 请求头里没有`If-Modified-Since`和`If-None-Match`
  • 再次请求,可以看到status还是200,说明从服务器重新获取资源内容了

    // 响应
    HTTP/1.1 200 OK
    Last-Modified: Thu, 27 Jul 2023 03:50:18 GMT
    ETag: "64c1e97a-e3"
    Cache-Control: no-store
    // 请求
    // 请求头里没有`If-Modified-Since`和`If-None-Match`,只有Cache-Control:max-age=0
    Cache-Control: max-age=0
  • 再请求几次结果也是一样的

配置max-age

修改配置max-age=240,也就是4分钟内如果命中缓存,就使用缓存

add_header 'Cache-Control' 'max-age=240';
  • 第一次请求资源

    // 响应头
    HTTP/1.1 200 OK
    Last-Modified: Thu, 27 Jul 2023 04:13:31 GMT
    ETag: "64c1eeeb-e3"
    Cache-Control: max-age=240
    // 请求
    // 请求头里没有`If-Modified-Since`和`If-None-Match`
  • 四分钟内再次请求,status是304

    // 响应头
    HTTP/1.1 304 Not Modified
    Last-Modified: Thu, 27 Jul 2023 04:13:31 GMT
    ETag: "64c1eeeb-e3"
    Cache-Control: max-age=240
    // 请求头
    Cache-Control: max-age=0
    If-Modified-Since: Thu, 27 Jul 2023 04:13:31 GMT
    If-None-Match: "64c1eeeb-e3"

    似乎没有直接使用本地缓存,默认的请求头的max-age=0,表示不使用强缓存,但允许协商缓存,所以返回响应码是304

    使用Chrome插件ModHeader来修改请求头Cache-Control: max-age=240,还是304?怀疑是缓存不够用,因为理论上而言,应该使用本地缓存

  • HTML增加外部脚本的引用,请求脚本时可以命中缓存

    试了几次修改HTML,还是不行,给HTML增加外部的js脚本,发现脚本可以被缓存,显示了from memory cache。可能是浏览器默认的策略,请求页面的时候默认为no-cache直接进入协商缓存阶段;请求其他类型资源的时候才会优先去匹配本地缓存。

    Request URL:  http://localhost:8080/cache-demo/main.js
    Status Code:  200 OK (from memory cache)

    此时设置max-age=240,发现脚本没有按照我们预期设置的max-age过期而重新获取新的资源,这应该是Chrome本身对资源加载的一个优化,以达到充分利用本地缓存的目的,这也解释了有些情况下,前端代码重新部署后,无法加载到最新内容的原因,一来HTML内容没有更改,所以默认的协商缓存返回304,读取了本地资源;二来HTML引用的js脚本也在本地可以找到缓存,就没有向浏览器发起请求。最终导致读取到的是旧的资源内容,需要等待几分钟才能读取到新的内容。

    为了解决这个问题,可以在nginx配置显式的声明no-cache

    add_header 'Cache-Control' 'no-cache, max-age=240';

    这样请求js脚本就会直接进入协商缓存,读取到新的资源内容。

结论

根据这个测试结果,为了保证内容的时效性,建议给资源所在服务器增加no-cache的配置,并且给HTML页面所在服务器增加no-store的配置,因为大多Vue或者react单页应用打包构建出来的HTML页面内容很少,往往只有一个div,配置了no-store可以保证请求到最新的资源,因为页面内容极少,正常情况下资源返回也非常快。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
东方客主 东方客主
4年前
一文读懂http缓存(超详细)
前端缓存前端缓存可分为两大类:http缓存和浏览器缓存。我们今天重点讲的是http缓存,所以关于浏览器缓存大家自行去查阅。下面这张图是前端缓存的一个大致知识点:(https://imghelloworld.osscnbeijing.aliyuncs.com/3e161cfcbe560b9608064ec91077346
Wesley13 Wesley13
3年前
DNS解析全过程分析
DNS解析过程!(https://oscimg.oschina.net/oscnet/cea17faee069f853dc0dcb337b3371124d3.png)1.检查浏览器缓存中是否缓存过该域名对应的IP地址用户通过浏览器浏览过某网站之后,浏览器就会自动缓存该网站域名对应的IP地址,当用户再次访问的时候
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
CDN与缓存的归纳理解
缓存是什么缓存是一个到处都存在的用空间换时间的例子。通过使用多余的空间,我们能够获取更快的速度。我们通常意义上说的缓存主要包含两部分。第一个是用户浏览器端的缓存,第二个是服务器端为了提高访问速度而加的CDN。首先,看看没有网站没有接入CDN时,用户浏览器与服务器是如何交互的:!q111.png(https://oscimg.o
Stella981 Stella981
3年前
Http 缓存策略
1)浏览器缓存策略浏览器每次发起请求时,先在本地缓存中查找结果以及缓存标识,根据缓存标识来判断是否使用本地缓存。如果缓存有效,则使用本地缓存;否则,则向服务器发起请求并携带缓存标识。根据是否需向服务器发起HTTP请求,将缓存过程划分为两个部分:强制缓存和协商缓存,强缓优先于协商缓存。强缓存,服务器通知浏览器一个缓存时间,在
Wesley13 Wesley13
3年前
JS浏览器不缓存页面的几种方法
我们需不需要浏览器缓存?浏览器缓存,有时我们需要,有时我们不需要,就比如股票类型的网页就需要实时刷新数据,不能让页面从缓存里读取数据,如果对于一些不需要实时更新数据的网站来说,浏览器缓存可以提高加载速度,带来更好的用户体验,到底需不需要浏览器缓存,让我们自己操作!meta方法//不缓存<METAHTTPEQU
燕青 燕青
1年前
Macos专业的清理优化工具:BuhoCleaner for mac
是一款简单易用的清洁工具应用程序。它可以帮助您清理和优化您的设备,以提高性能和速度。BuhoCleaner可以扫描和删除不需要的文件和垃圾,包括缓存、残留文件、临时文件和无效的注册表项。它还可以清理浏览器历史记录、Cookie和缓存,以保护您的隐私。此外,
使用Scrapy进行网络爬取时的缓存策略与User-Agent管理
缓存策略的重要性缓存策略在网络爬虫中扮演着至关重要的角色。合理利用缓存可以显著减少对目标网站的请求次数,降低服务器负担,同时提高数据抓取的效率。Scrapy提供了多种缓存机制,包括HTTP缓存和Scrapy内置的缓存系统。HTTP缓存HTTP缓存是基于HT