页面性能之浏览器缓存

1. 前言
2. 缓存的位置
3. 缓存过程分析
4. 缓存的分类
5. 实际场景应用缓存策略
6. 用户行为对浏览器缓存的影响

一. 前言:

缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。

对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

接下来的内容中我们将通过缓存位置缓存策略以及实际场景应用缓存策略来探讨浏览器缓存机制。

二. 缓存的位置:

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。
  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache
1. Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

2. Memory Cache

Memory Cache 也就是内存中的缓存,主要包含的是当前页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。

3. Disk Cache

Disk Cache 也就是存储在硬盘中的缓存,它读取速度慢点,但是什么都能存储到磁盘中,比Memory Cache 胜在容量和存储时效性上。

在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache,关于 HTTP 的协议头中的缓存字段,我们会在下文进行详细介绍。

4. Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。

那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

三. 缓存过程分析:

浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求,那么`浏览器怎么确定一个资源该不该缓存,如何去缓存呢`?浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。具体过程如下图:

在这里插入图片描述

由上图我们可以知道:

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识

  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

以上两点结论就是浏览器缓存机制的关键,它确保了每个请求的缓存存入与读取

四. 缓存的分类:

#### 强缓存: **定义**:强制缓存,只要在有效的时间范围内就不会去服务器发送请求索取资源,而是直接用本地缓存的。

主要http字段
Expires Expires: Mon, 25 Jun 2029 06:17:58 GMT
Cache-Control Cache-Control: max-age=315360000

Expires和Cache-Control的区别

  • Expires指的是服务器的时间,而客户端时间和服务器时间可能不一致,因为有传输过程的时间消耗,而导致的不准,所以后来又有了Cache-Control。
  • Cache-Control指浏览器时间,并以这个为准,其中max-age的单位为秒。
  • 具体是存在Expires字段还是Cache-Control字段,根据服务器的配置来定,如果都存在,那么以Cache-Control为准。
二. 协商缓存:

定义
强制缓存的时间过期了,那么就需要去服务器去问一下,这个缓存的文件到底还能不能用。

主要字段
Last-Modified Last-Modified: Fri, 22 Feb 2019 06:52:34 GMT
If-Modified-Since

当服务器下发一个文件时,会在响应头返回一个Last-Modified字段,也就是一个时间戳,当强缓存过期时,浏览器发送请求的时候就会拿着If-Modified-Since这个字段去向服务器问这个缓存的文件还能不能继续使用,这个字段的时间戳是和下发的时间戳一样的,如果能用,OK,继续用缓存的,否则需要服务器重新下发。

如果修改时间变了,但是内容没有变,还得需要重新下发,所以这个字段是有缺点的,所以后来才有了后面的协商缓存字段Etag。
另外:Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了

Etag Etag: “196f-582760919f080”
If-None-Match

Etag是服务器下发文件的时候给请求头返回的一个字段,其值为一段哈希值 。
当强缓存过期时,浏览器拿着If-None-Match 这个字段,其实也就是Etag的哈希值去问服务器内容有没有变化,如果有变化(即两个哈希值不一样了),那么就需要重新下发文件了,否则继续使用之前缓存的。

实际查看:

在这里插入图片描述


cache-control:对应的值如下:no-cache、no-store、public、private、max-age

在这里插入图片描述

let http = require('http');
let path = require('path');
let fs = require('fs');
let express = require('express');
let md5 = require('md5');

let app = new express();

// app.set('views', path.join(__dirname, 'views'));
// app.set('view engine', 'ejs');

app.get('/', (req, res) => {
    res.sendFile(path.resolve(__dirname, './public/index.html'));
})
app.get('/style.css', (req, res) => {
    let relativePath = path.resolve(__dirname, 'public/style.css');
    //etag
    fs.readFile(relativePath, (err, content) => {
        let etag = md5(content); //可以以文件的内容是否修改对文件进行标记
        let ifNoneMatch = req.headers['if-none-match'];
        if (ifNoneMatch == etag) {
            res.writeHead(304, 'Not Modified');
            res.end();
        } else {
            res.setHeader('ETag', etag); //if-none-match
            //res.setHeader('Cache-Control', 'public,max-age=10');//设置最大缓存时间限制(强制缓存)
            //res.setHeader('Expires',new Date(Date.now() + 30000));
            //为了兼容低版本HTTP通常也会设置响应头Expires,Expires的时间为GMT时间格式
            res.writeHead(200, 'OK');
            res.end(content);
        }
    })

    //last-modified-since
    /* fs.stat(relativePath, (err, stat) => {
        if (err) return;
        let ifModifiedSince = req.headers['if-modified-since'];
        let mtime = stat.mtime.toUTCString();
        if (ifModifiedSince == mtime) {//文件最后一次修改时间(文件虽然修改,但是里面的内容可能没有发生变化)
            res.writeHead(304, 'Not Modified');
            res.end();
        } else {
            res.setHeader('Last-Modified', mtime);//if-modified-since
            //res.setHeader('Cache-Control', 'public,max-age=10');//设置最大缓存时间限制
            //res.setHeader('Expires',new Date(Date.now() + 30000));
            res.writeHead(200, 'OK');
            res.end(content);
        }

    }) */

})

app.listen(3000);

五. 实际场景应用缓存策略:

1. 频繁变动的资源

Cache-Control: no-cache

对于频繁变动的资源,首先需要使用Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

注意:
no-cache并不表示不缓存,no-cache表示每次使用缓存时,都要向服务器验证。当服务器表示资源仍然可用时,就使用缓存。no-store才表示不缓存。

2. 不常变化的资源

Cache-Control: max-age=31536000

通常在处理这类资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。
在线提供的类库 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用这个模式。

HTTP缓存,主要有两种缓存:强缓存和对比缓存(也叫协商缓存)
  • 强缓存:
    只要请求了一次,在有效时间内,不会再请求服务器(请求都不会发起),直接从浏览器本地缓存中获取资源。主要字段有(expires:date(过期日期)、cache-control: max-age=time(毫秒数,多久之后过期) |no-cache|no-store)。如果expires和cache-control同时存在,cache-control会覆盖expires。建议两个都写,cache-control是http1.1的头字段,expires是http1.0的头字段,都写兼容会好点。

  • 协商缓存:
    无论是否变化,是否过期都会发起请求,如果内容没过期,直接返回304,从浏览器缓存中拉取文件,否则直接返回更新后的内容。
    主要字段有:
    1、服务器端返回字段 Etag: xxxx (一般为md5值) 对应客户端匹 配字段为, If-None-Match: w/xxx(xxx的值和上面的etag的xxx一样则返 回304,否则返回修改后的资源)。
    2、服务器端返回字段:Last-Modifieddate(日期),对应客户端匹配字段If-Modified-Since:date(如果服务器date小于等于客户端请求date则返回304,否则返回修改后的资源))

HTTP缓存优先级问题

1、强缓存和对比缓存同时存在,如果强缓存还在生效期则强制缓存覆盖对比缓存,对比缓存不生效;如果强缓存不在有效期,对比缓存生效。即:强缓存优先级 > 对比缓存优先级
2、强缓存expires和cache-control同时存在时,则cache-control会覆盖expires,expires无论有没有过期,都无效。 即:cache-control优先级 > expires优先级。
3、对比缓存Etag和Last-Modified同时存在时,则Etag会覆盖Last-Modified,Last-Modified不会生效。即:ETag优先级 > Last-Modified优先级。

当然还有一种缓存pragma,和cache-control类似,前者是http1.0内容后者是http1.1内容,并且pragma优先级 > cache-control优先级,不过前者目前基本不使用。

针对不同的项目,如果css和js在打包时加了md5值,建议直接使用强缓存,并且expires和cache-control同时使用,建议设置时长为7天较为妥当。图片文件如果没有加md5值,建议采用对比缓存,html文件也建议采用对比缓存。


一般情况html文件不做强缓存,因为一旦更新了用户可能看不到最新内容;而对于静态资源,例如css、js、图片等可以设置强缓存,因为一般静态资源文件都带上hash戳。例如如下设置:

if ($request_filename ~ .*\.(?:js|css|jpg|jpeg|gif|png|svg|svgz|mp4|ogg|ogv|webm|woff2|ttf)$)
{
   add_header  Cache-Control   max-age=3600;
}

注:(?:)为非捕获性匹配,参考 链接


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

用户在浏览器采用一些操作,例如,返回上一阶段,下一阶段,刷新页面,强制刷新
等操作,这些对于一些缓存属性的影响是不一样的。下面将进行详细解读。

注:不同浏览器的刷新策略是不一样的,下面的不用过渡纠结。

  1. 刷新(仅仅是F5Ctrl+R刷新):此时对于cache-control/Expires是不生效的,但是last-modified/Etag都生效的,所以此时会向服务器发起请求,用来判断目标文件是否发生变化。
  2. 强制刷新(F5刷新+ctrlCtrl+R+Shift):此时对于cache-control/expires和last-modified/Etag都不生效,此时必须从服务器拿到新数据。
  3. 回车或者转向:此时所有的缓存都生效。

关于Chrome浏览器的正常重新加载,硬性重新加载,清空缓存并硬性重新加载的区别:
在这里插入图片描述

  • Normal Reload (Ctrl + R / F5): 正常刷新,使用缓存数据。
  • Hard Reload (Ctrl + Shift + R / Shift + F5):强制浏览器重新下载并加载内容。
  • Empty Cache and Hard Reload:完全清除页面的缓存并重新下载所有内容。

第二种方式与第三种方式的区别:如果使用第二种方式刷新网页,虽然浏览器会强制的重新下载页面资源,但其可控的资源只是刷新后首次显示的界面资源,对于一些触发后由js控制的动态页面,无法强制重新下载,触发js事件后可能还是会从缓存中读取数据填充页面,因为此时已经脱离hard reload的作用范围。而Empty Cache and Hard Reload则直接清空缓存,其中就包括了js可能用到的资源,这种方法进行的更加彻底,可以做到完全不从缓存中读取数据


请求头里的Cache-Control是no-cache,是浏览器通知服务器:本地没有缓存数据
响应头中的 Cache-Control:max-age=259200 是通知浏览器:259200 秒之内别来烦我,自己从缓冲区中刷新

HTTP请求头和响应头中cache-control的区别


问:
在html文件中的meta标签中设置cache-control,但是响应头里没有返回相关的cache-control

答:
前端的这个配置,只是个历史遗留的产物,现在基本已经淘汰了

从浏览器来讲,很简单,这个只是IE时代的私有属性,在IE9以前支持的,而现在主流的chrome\firefox\safari,包括IE9-11都不再支持了,如果应用需要兼容低版本的IE浏览器(如银行、zf等),可以加上这个东西,否则就完全没有必要了;

从http来讲,这东西是http1.0时代的产物,因为http1.0里关于缓存可设定的内容太少(要知道,http0.9压根就不支持服务端的response.header,http1.0虽是添加了header,但除了status code,也没多少可以设置的),而且http1.1发布早期,并不是所有浏览器都支持,所以把控制缓存的cache-control放到了前端html的页面中。

Cache-control:public,指定是否是共享缓存,如果设置Cache-control的值设置为public,则表示多个浏览器之间可以共同使用该资源缓存。
如果没有指定Cache-control是为private还是public,则默认是public。
private表示仅浏览器可以缓存。


Nginx add_header和缓存控制

向响应的header中增加字段,最常用的就是“add_header Cache-Control max-age=0”。

如何配置nginx的expires功能

expires起到控制页面缓存的作用,合理的配置expires可以减少很多服务器的请求
要配置expires,可以在http段中或者server段中或者location段中加入

expires 指令可以控制 HTTP 应答中的“ Expires ”和“ Cache-Control ”的头标(起到控制页面缓存的作用)

语法:expires [time|epoch|max|pff]

默认值:off

time:可以使用正数或负数。“Expires”头标的值将通过当前系统时间加上设定time值来设定。
time值还控制"Cache-Control"的值:
负数表示no-cache
正数或零表示max-age=time


总结:

在HTML中通过<meta http-equiv=‘cache-control’ content=‘no-cache’ />设置缓存是不靠谱的,甚至是无效的。

HTML默认的response header是no-cache,也就是不强缓存。

对于ajax请求,我们可以在response或者request中来正常设置。

在这里插入图片描述

在这里插入图片描述

参考链接:

  1. 浏览器 缓存 面试 参数解析 Cache-Control Expires ETag Last-Modified
  2. http状态码304缓存机制(强缓存和协商缓存-304)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

. . . . .

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值