浏览器缓存原理

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

目的

通常说到浏览器缓存,大多是和性能优化有关,使用缓存,通常是两个主要目的,第一是提高访问速度,第二是减少网络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返回。

makefile 复制代码
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获得一个标识字符串,类似:

vbnet 复制代码
Etag: "dec8d6f46497a9c6b4dff4e237cabe5d"

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

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

sql 复制代码
If-None-Match:  "dec8d6f46497a9c6b4dff4e237cabe5d"

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

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

如何配置?

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

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

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

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

      yaml 复制代码
      Cache-Control: no-store
    • 如果是可复用资源

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

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

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

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

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

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

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

实操

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

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

当不做配置时

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

    sql 复制代码
    // 响应码 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,说明资源内容没有更改

    sql 复制代码
    // 响应码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

    yaml 复制代码
    // 响应
    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

arduino 复制代码
add_header 'Cache-Control' 'no-store';
  • 第一次访问

    perl 复制代码
    // 响应
    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,说明从服务器重新获取资源内容了

    yaml 复制代码
    // 响应
    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分钟内如果命中缓存,就使用缓存

arduino 复制代码
add_header 'Cache-Control' 'max-age=240';
  • 第一次请求资源

    arduino 复制代码
    // 响应头
    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

    yaml 复制代码
    // 响应头
    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直接进入协商缓存阶段;请求其他类型资源的时候才会优先去匹配本地缓存。

    bash 复制代码
    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

    arduino 复制代码
    add_header 'Cache-Control' 'no-cache, max-age=240';

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

结论

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

相关推荐
辻戋2 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保2 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun3 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp3 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.4 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl6 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫7 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友7 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理9 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻9 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js