浏览器缓存策略

image

最近在对项目做 IE 11 兼容,由 IE 的缓存问题,引发我对于浏览器缓存策略的思考。

缓存类型

web缓存主要可以分为下面几类:

  • 客户端缓存
  • 服务端缓存
  • 数据库缓存

这里我们主要关注客户端,也就是浏览器缓存

浏览器和服务器通信是通过 HTTP 协议,浏览器向服务器发起 HTTP 请求,服务器作出响应。当再次发起请求的时候,可以直接读取缓存中的数据,减少网络带宽的消耗,提升页面的访问速度。

根据是否重新发起 HTTP 请求,可以将浏览器缓存分为两种:强制缓存协商缓存

1.强制缓存

与强制缓存有关的 HTTP 头部有 ExpiresCache-Control

Expires

Expires 响应头包含一个 HTTP 日期(GMT 时间,非本地时间),表示资源过期的时间。

当设置无效值,例如 0,表示资源立即过期,即不使用缓存。

//...
const getGMT = () => `${moment().utc().add(1, 'm').format('ddd, DD MMM YYYY HH:mm:ss')} GMT`

app.get('/expries', (req, res) => {
  res.setHeader('Expires', getGMT());
  res.end('ok')
});

这里使用 express 创建了一个 web 服务,在 header 中添加了 Expires 响应头,利用 moment 转化为相应的 GMT 格式,设置为 10s 后过期,可以看到首次请求时向服务端发起了 HTTP 请求,第二次则使用了缓存(disk cache),超过 10s 之后再请求时(第三次)缓存过期,重新向服务端发起 HTTP 请求。

image

请求时带上 Expries 请求头:

image

Cache-Control

Cache-Control 是一个通用首部,既可以设置在请求头中,也可以设置在响应头中,常用的取值包括以下几种:

Cache-Control 取值 含义
no-store 绝对禁止缓存
no-cache 会被缓存,但是立刻过期,要求将请求提交给原始服务器进行验证,相当于 max-age=0
private 只有浏览器可以缓存,禁止代理服务器、CDN等中间人缓存
public 资源可以被任何对象缓存
max-age 表示资源被缓存的最大时间,单位秒;当设置该值时,Expries 头部会被忽略

其中privatepublic只能用于响应头部中

image

2.协商缓存

在强制缓存中,我们根据时间来判断资源是否过期,这会存在一定弊端,当过期时间到了,即使服务端资源未改动,也会重新获取。由此我们引进了协商缓存的概念,协商缓存需要浏览器和服务器共同实现,与协商缓存有关的响应头部字段主要为以下两组:

  • Last-ModifiedIf-Modified-Since
  • ETagIf-None-Match

Last-Modified

Last-Modified 表示资源最后的修改时间(GMT 格式),具体过程如下:

  1. 首次请求,服务端返回 Last-Modified 响应头部,告诉浏览器该资源的最后修改时间
  2. 再次请求,如果浏览器缓存未过期,直接读取缓存中的资源(disk cache
  3. 当浏览器的缓存资源过期,此时再发起 HTTP 请求时,会自动带上 If-Modified-Since 这个请求头部,它的值即为上一次请求响应的 Last-Modified,服务端比较两个字段的值,如果一致,说明资源未改动,返回 304,否则返回更改后的资源。
image

可以看到再次请求时自动加上 If-Modified-Since 请求头部:

image

服务端实现如下:

const filePath = path.join(__dirname, '../static/index.html')

app.get('/lastModified', (req, res) => {
  const stat = fs.statSync(filePath);
  const file = fs.readFileSync(filePath);
  const lastModified = stat.mtime.toUTCString();

  res.setHeader('Cache-Control', 'public,max-age=10');

  if (lastModified === req.headers['if-modified-since']) {
    res.writeHead(304, 'Not Modified');
    res.end();
  } else {
    res.setHeader('Last-Modified', lastModified);
    res.writeHead(200, 'OK');
    res.end(file);
  }
});

ETag

当资源发生多次改动,但是资源内容未改变时,此时服务器仍需要重新返回资源。为了提升判断的精确度,引入 ETag 响应头部,表示资源特定版本的标识符,当文件内容未发生变化时,该标识符的值不会改变。具体过程如下:

  1. 首次请求,服务端返回 ETag 响应头部,告诉浏览器该资源的特殊标识
  2. 再次请求,如果浏览器缓存未过期,直接读取缓存中的资源(disk cache
  3. 当浏览器的缓存资源过期,此时再发起 HTTP 请求时,会自动带上 If-None-Match 这个请求头部,它的值即为上一次请求响应的 ETag,服务端比较两个字段的值,如果一致,说明资源未改动,返回 304,否则返回更改后的资源。
image

当文件发生变化时,响应头部的 ETag 和请求头部的 If-None-Match 不一致:

image

服务端实现如下:

const filePath = path.join(__dirname, '../static/index.html')

// 创建 md5 加密
const cryptoFile = (file) => {
  const md5 = crypto.createHash('md5');
  return md5.update(file).digest('hex');
}

app.get('/eTag', (req, res) => {
  const file = fs.readFileSync(filePath);
  const eTag = cryptoFile(file)

  res.setHeader('Cache-Control', 'public,max-age=10');

  if (eTag === req.headers['if-none-match']) {
    res.writeHead(304, 'Not Modified');
    res.end();
  } else {
    res.setHeader('ETag', eTag);
    res.writeHead(200, 'OK');
    res.end(file);
  }
})

3.其他

Pragma

在 HTTP/1.0 时期存在一个通用首部 Pragma ,当它的值为 no-cache 时,与 Cache-Control: no-cache 的行为一致。它在“请求-响应”链中可能会有不同的效果,现在一般用于向后兼容只支持 HTTP/1.0 的客户端。

Chrome 下测试,在请求头部/响应头部中设置 Pragma: 'no-cache' 均可以实现禁用缓存:

image

但在 IE 11 下,当 Pragma 置于响应头部时并未生效,可以在 IE 11 下运行测试代码进行验证。

cache

在 chrome 下控制台可以看到浏览器本地缓存分为两类:memory cachedisk cache,即内存缓存磁盘缓存

  • 内存缓存主要包含当前页面已经加载的资源,例如图片、脚本等,当关闭TAB页时,内存中的缓存将被释放。
  • 磁盘缓存读取较内存缓存慢,但是胜在时效性和存储容量上,页面关闭后缓存依然存在。

那么浏览器是如何区分哪些资源存放在内存中,哪些又存在磁盘中呢?

其实这个问题没有一个标准答案,普遍认为和系统当前内存的使用情况有关,如果当前系统内存使用率高,那么会优先存储在磁盘中;另外一个就是对于大文件,一般存储在磁盘中。

缓存优先级

关于优先级,强制缓存的优先级总是大于协商缓存,只有在强制缓存失效后才会发起请求进行协商缓存;

而在协商缓存中,Last-Modified表示的是一个 GMT 格式的时间,只能精确到秒,因此 ETag 的精确度要高于 Last-Modified,但同时每次进行 hash 运算生成标识也会带来额外的开销。二者都存在时,服务端应以 ETag 为准。

总的优先级如下:

Pragma > Cache-Control > Expries > ETag > Last-Modified

Chrome下验证,当 Pragmano-cacheCache-Control 设置 1000s 缓存时,浏览器会禁用缓存:

image

同样,设置响应头为 Cache-Control: 'no-cache'Expries1000s 后过期,浏览器依然禁用缓存:

image

缓存过程

整体的缓存过程如下:

image

IE 下的缓存

兼容 IE 11 的过程中踩过一些坑,在实际项目中遇到的印象比较深刻的问题是下面这个:

由于 IE 对 GET 接口的缓存,当用户首次进入系统时,因为未登录跳转至sso,登录成功之后仍然返回的是缓存中的未登录,导致登录之后出现闪屏,在原系统和sso之间不停来回跳转。

另外,由于 IE 浏览器打开控制台之后默认开启始终从服务端刷新,在 debug 阶段着实给我造成了不小的困扰,后来放弃使用控制台,通过抓包工具Charles进行截取、分析,这才定位到问题。

image

IE 缓存问题

究其原因,是 IE 对于 GET 请求的缓存策略问题:

  • 对于首次请求,会对服务器发起请求
  • 对于非首次请求,会直接从缓存中读取数据

多次发起 GET 请求时,若 url 未发生变化,IE 则认为这是非首次请求,直接读取缓存。

解决方法

1. 在 url 中加入随机标识

通过在 get 请求的 url 中加入随机标识,例如时间戳、随机数等,来达到变更 url 的目的,此时浏览器不会从缓存中读取数据

2. 设置 header 响应头部

服务端设置响应头部禁止浏览器缓存

{
  'Cache-Control': 'no-cache',
  'Pragma': 'no-cache',
  'Expires': -1,
}

3. 设置 header 请求头部

在实际项目中我采用的是这种解决方案

{
  'Cache-Control': 'no-cache',
  'Pragma': 'no-cache',
}

4. 更改为 post 请求(不推荐)

参考

原文链接:浏览器缓存策略

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 今天看奇舞团推了篇文章讲缓存策略的,讲的挺不错,记录一下。 原文地址就在下面。 总结: 缓存分为强缓存和协商缓存...
    NowhereToRun阅读 4,633评论 1 7
  • 当浏览器请求一个网站的时候,会加载各种各样的资源,比如:HTML文档、图片、CSS和JS等文件。对于一些不经常变的...
    LUGY阅读 851评论 0 0
  • 来自公众号:前端下午茶链接:https://segmentfault.com/a/1190000012573337...
    码农小光阅读 452评论 0 5
  • 参考《深入理解浏览器的缓存机制》进行整理 前言 缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略...
    琢磨先生lf阅读 718评论 1 1
  • 我知道这个时候写下这些都是被人耻笑的,也知道即便你们看到的时候也会不为所动。 但是也许在这个...
    不俗小七阅读 633评论 3 9