乐趣区

关于前端:面试常客HTTP-缓存

速度、速度,还是速度,一个网站要想体验好,就必须在第一工夫以最快的速度显示进去。mysql 查问慢,就加一层 redis 做缓存,网站资源加载慢,怎么做,应用 HTTP 缓存

HTTP 缓存自 HTTP/1.0 就开始有,为的是缩小服务器压力,放慢网页响应速度

缓存操作的指标

HTTP 缓存只能存储 GET 申请的响应,而对其余类型的申请无能为力

缓存发展史

HTTP/1.0 提出缓存概念,即强缓存 Expires 和协商缓存 Last-Modified。后 HTTP/1.1 又有了更好的计划,即强缓存 Cache-Control 和协商缓存 ETag

为什么 Expires 和 Last-Modified 不实用呢?

Expires 即过期工夫,但问题是这个工夫点是服务器的工夫,如果客户端的工夫和服务器工夫有差,就不精确。所以用 Cache-Control 来代替,它示意过期时长,这就没歧义了

Last-Modified 即最初批改工夫,而它能感知的单位工夫是秒,也就是说如果在 1 秒内扭转屡次,内容文件尽管扭转了,但展现还是之前的,存在不精确的场景,所以就有了 ETag,通过内容给资源打标识来判断资源是否变动

以下表格利于比照了解

版本 强缓存 协商缓存
HTTP/1.0 Expires Last-Modified
HTTP/1.1 Cache-Control ETag

两大缓存类型比照

前文已介绍不同版本下的缓存类型。过后提了有一句强缓存和协商缓存,但没具体介绍。当初来讲讲这两种缓存类型

强缓存

Cache-Control

  • HTTP/1.1
  • 通过过期时长管制缓存,对应的字段有很多,例如 max-age

    • 例如 Cache-Control: max-age=3600,示意缓存工夫为 3600 秒,过期生效
  • 缓存申请指令:

    • Cache-Control: max-age=<seconds>
      Cache-Control: max-stale[=<seconds>]
      Cache-Control: min-fresh=<seconds>
      Cache-control: no-cache
      Cache-control: no-store
      Cache-control: no-transform
  • 缓存响应指令:

    • Cache-control: must-revalidate
      Cache-control: no-cache
      Cache-control: no-store
      Cache-control: no-transform
      Cache-control: public
      Cache-control: private
      Cache-control: proxy-revalidate
      Cache-Control: max-age=<seconds>
  • 其中关键点:

    • Cache-control: no-cache

      • 跳过以后的强缓存,发送 HTTP 申请(如有协商缓存标识即间接进入 协商缓存阶段
      • no-cache 的含意和 max-age=0 一样,即跳过强缓存,强制刷新
    • Cache-control: no-store

      • 不应用缓存(包含协商缓存)
    • Cache-Control: public, max-age=31536000

      • 个别用于缓存动态资源
      • public:响应能够被两头代理、CDN 等缓存
      • private:专用于集体的缓存,两头代理、CDN 等能换缓存此响应
      • max-age:单位是秒
  • 更多指令参考指令大全

Expires

  • HTTP/1.0
  • 语法:

    • Expires: <http-date>
  • 即过期工夫,存在于服务器返回的响应头里

    • Expires: Mon, 11 Apr 2022 06:57:18 GMT
    • 示意资源在 2022 年 4 月 11 号 6 点 57 分过期,过期了就会往服务端发申请
  • 如果在 Cache-Control 响应头设置了 “max-age” 或者 “s-max-age” 指令,那么 Expires 头会被疏忽
  • 毛病:服务器工夫与浏览器工夫可能不统一
  • 更多指令参考指令大全

Cache-Control VS Expires

  • Cache-Control 较之 Expires 更为精准
  • 同时存在时,Cache-Control 优先级大于 Expires
  • Expires 是 HTTP/1.0 提出,其浏览器兼容性更好,Cache-Control 是 HTTP/1.1 提出,可同时存在,当有不反对 Cache-Control 的浏览器时会以 Expires 为准

协商缓存

协商缓存须要配合强缓存应用,应用协商缓存的前提是设置强缓存设置 Cache-Control: no-cache或者 pragma: no-cache或者 max-age=0 通知浏览器不走强缓存

pragma 是 HTTP/1.0 中禁止网页缓存的字段,其取值为 no-cache 和 Cache-Control 的 no-cache 成果一样

ETag/If-None-Match

  • HTTP/1.1
  • 即生成文件惟一标识来判断是否过期。只有内容扭转,这个值就会变
  • If-None-Match 配合,ETag 是申请服务器后返回给每个资源文件的惟一标识,客户端会将此标识存在客户端(即浏览器)中,下次申请时会在申请头的 If-Nono-Match 中将其值带上,服务器判断 If-None-Match 是否与本身服务器上的 ETag 统一,如果统一则返回 304,重定向跳转应用本地缓存;不统一,则返回 200,将最新资源返回给客户端,并带上 ETag
  • 更多指令参考指令大全

Last-Modified/If-Modified-Since

  • HTTP/1.0
  • 最初批改工夫,即通过最初批改工夫来判断是否过期。在浏览器第一次给服务器发送申请后,服务器会在响应头上加上这个字段
  • If-Modified-Since 配合,客户端拜访服务器资源时,服务器端会将 Last-Modified 放入响应头中,即这个资源在服务器上的最初批改工夫,客户端缓存这个值,等下次申请这个资源时,浏览器会检测到申请头中的 Last-Modified,于是乎增加 If-Modified-Since,如果 If-Modified-Since 的值与服务器中这个资源的最初批改工夫统一,则返回 304,重定向跳转应用本地缓存;不统一,则返回 200,将最新资源返回给客户端,并带上 Last-Modified
  • 毛病:

    • 文件尽管被批改,但最初的内容没有变动,这样文件批改工夫还是会更新
    • 有些文件批改频率在秒以内,这样以秒粒度来记录就不实用了
    • 有些服务器无奈精准获取文件的最初批改工夫
  • 更多指令参考指令大全

ETag VS Last-Modified

  • 精确度

    • ETag > Last-Modified。ETag 是通过内容给资源打标识来判断资源是否变动,而 Last-Modified 不一样,在某些场景下准确度会生效。例如编辑文件,然而文件内容未变,缓存会生效;或者在 1 秒内扭转屡次,Last-Modified 能感知的单位工夫是秒
  • 性能

    • Last-Modified > ETag。Last-Modified 仅仅记录一个工夫点,而 ETag 须要依据文件的具体内容生成哈希值
  • 如果两个都反对的话,服务器会优先选择 ETag

协商缓存的条件申请

前文说到协商缓存是在申请头增加 If-None-MatchIf-Modified-Since,这些申请头是什么,增加有什么用?

强缓存是通过具体工夫到期或过期时长来管制缓存,这就有个问题了,如果其中的一些文件批改了,因为强缓存,浏览器展现的还是原来的数据,所以对那种常变动的数据不能应用强缓存做缓存策略,于是乎,就有了协商缓存,通过文件变动通知浏览器缓存生效,应用前需去服务器验证是否是最新版?

这样,浏览器就要间断发送两个申请来验证:

  1. 先是 HEAD 申请,获取资源的批改工夫、hash 值等元信息,而后与缓存数据比拟,如果没有改变就应用缓存
  2. 否则就再发一个 GET 申请,获取最新的版本

但这样的两个申请的网络老本太高,所以 HTTP 协定就定义了一系列 If 结尾的条件申请字段,专门用来查看验证资源是否过期,把两个申请合并在一个申请中做。而且验证的责任也交给服务器

  • If-Modified-Since:和 Last-modified 比拟,是否曾经批改了
  • If-None-Match:和 ETag 比拟,惟一标识是否统一
  • If-Unmodified-Since:和 Last-modified 比照,是否批改
  • If-Match:和 ETag 比拟是否匹配
  • If-Range

其中,最常见的当属是 If-Modified-Since 和 If-None-Match。它们别离对应 Last-Modified 和 ETag。须要第一次的响应报文事后提供 Last-Modified 和 ETag,而后第二次申请时就能够带上缓存里的旧址,验证资源是否是最新的

如果资源没有变,服务器就回应一个 304 Not Modified,示意缓存仍然无效,浏览器就能够更新一个有效期,而后应用缓存了

什么时候用强缓存,什么时候用协商缓存?

首先强缓存的权重大于协商缓存,当强缓存存在时,协商缓存只能看着;其次 HTTP/1.1 中的缓存标识符大于 HTTP/1;所以当 Cache-Control 存在时,看它的,如果它不存在,则看 Expires,如果将强缓存设置为 Cache-Control:no-cacheCache-Control:max-age=0pragma: no-cache,即通知浏览器不走强缓存,则进入协商缓存。

判断上次响应中是否有 ETag,如果有,则发动申请,申请头中带有条件申请 If-None-Match,如果没有,则再判断上次响应中是否有 Last-Modified,如果有,则发动申请头中带 If-Modified-Since 的条件申请,如果没有,则阐明没有协商缓存,发动 HTTP 申请即可。无论是带If-None-Match 的申请还是 If-Modified-Since 的申请,都会返回状态(由服务器端判读资源是否变动),如果是 304,阐明缓存资源未变,应用本地缓存;如果是 200,则阐明资源扭转,发动 HTTP 申请,并记住响应头中的 ETag/Last-Modified

大抵流程图如下所示:

那么哪些资源要采纳强缓存,哪些资源采纳协商缓存呢?

像动态资源这类咱们长期不会去变动的资源应该用强缓存,不难理解;而像咱们常批改的文件应该采纳协商缓存,如果资源没变,那么当用户第二次进去还是用该资源,如果资源批改,用户进入发动 HTTP 申请获取最新资源

咱们在拜访网站时,如果留心都能在 F12 中察看到一二。如图所示,我的五年前端三年面试放在 github 服务器上,F12 进入 Network 中,能看到返回头中的信息。Cache-Control、Expires、ETag、Last-Modified 都存在

缓存地位

上文中常提到无论应用强缓存还是协商缓存,都会从浏览器本地中获取,那么浏览器的本地存储是存在哪里,他们又有什么分类呢?

依照缓存地位分类,分为到处,Memory Cache(内存缓存)、Disk Cache(硬盘缓存)、Service Worker、Push Cache

Memory Cache

因为内存无限,并不是所有的资源文件都会放在内存里缓存,它次要用来缓存有 preloader 相干指令的资源,比方<link rel="prefetch">。preloader 能够一边解析 js/css 文件,一边网络申请下一个资源

Disk Cache

磁盘上的缓存。在所有浏览器缓存中,disk cache 覆盖面最大,它会依据 HTTP Header 中的字段判断哪些资源须要缓存,哪些资源曾经过期须要从新从服务器端申请

Service Worker

独立线程,借鉴了 Web Worker 的思路。即让 JS 运行在主线程之外,因为它脱离浏览器窗口,因为无奈间接拜访 DOM,然而它还是能做很多事件,如

  • 离线缓存,Service Worker Cache
  • 音讯推送
  • 网络代理
  • 它是 PWA 的重要实现机制

Push Cache

即推送缓存,浏览器中的最初一道防线,HTTP2 中的内容

优先级:Service Worker–>Memory Cache–>Disk Cache–>Push Cache。

实际

说了这么多理论知识,等实战的时候却一头雾水,怎么破?

以上皆为口舌之辩,唯有实际出真章(以上皆为面试之辩,唯有实际出本事)

目前前端我的项目都是以 webpack 或类 webpack 工具库打包,在 webpack 中配置哈希,前端方面的缓存工作就实现了

咱们要实现的成果是:

  • HTML:协商缓存
  • CSS、JS、图片等资源:强缓存,文件名带上 hash

webpack 中的哈希有三种:hash、chunkHash、contentHash

  • Hash:和整个我的项目的构建相干,只有我的项目文件有扭转,整个我的项目构建的 hash 值就会扭转
  • chunkHash:和 webpack 打包的 chunk 无关,不同的入口会生成不同的 chunkHash 值
  • contentHash:依据文件内容来定义 hash,文件内容不变,则 contentHash 不变

这边须要把 CSS 用 contentHash 解决,其余资源用 chunkHash 做解决

非前端工程化我的项目

即传统的前端页面,个别放在动态服务器中,那么就要对批改的文件做版本控制,例如在入口文件 index.js 上加版本号(index-v2.min.js)或者加工夫戳(time=1626226),以此做缓存策略

后端缓存实际

真正起到缓存作用的是在后端,后端来设置缓存策略,通知浏览器是否做缓存。这里咱们对强缓存和协商缓存做个 demo 来试验下,

强缓存计划

代码如下:

const express = require('express');
const app = express();
var options = { 
  etag: false, // 禁用协商缓存
  lastModified: false, // 禁用协商缓存
  setHeaders: (res, path, stat) => {res.set('Cache-Control', 'max-age=10'); // 强缓存超时工夫为 10 秒
  },
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3008);

PS:代码起源自:图解 HTTP 缓存,在做测试时,须要留神,强缓存下,刷新页面是测不进去,点击后返回方能无效

协商缓存计划

代码如下:

const express = require('express');
const app = express();
var options = {
    etag: true, // 开启协商缓存
    lastModified: true, // 开启协商缓存
    setHeaders: (res, path, stat) => {
        res.set({
            'Cache-Control': 'max-age=00', // 浏览器不走强缓存
            'Pragma': 'no-cache', // 浏览器不走强缓存
        });
    },
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3001);

成果如下:

总结

HTTP 为什么要缓存,为了分担服务器压力,也为了让页面加载更快

有什么伎俩?HTTP 的强缓存和协商缓存,强缓存作用于那些不怎么变动的资源(如引入的库,js,css 等),协商缓存实用常更新的文件(例如 html)

强缓存是什么?在 HTTP/1.0 中以 Expires 为根据,但它不精确,HTTP 协定升级成 1.1 后,用新标识符 Cache-Control 来代替,但两者能够同时存在,Cache-Control 的权重更大一些

协商缓存是什么?在 HTTP/1.0 中以 Last-Modified 为根据,即最初过期批改工夫,它也不精确,HTTP 升级成 1.1 后,用新标识符 ETag 来代替,两者可同时存在,后者的权重更大

无论是 Expires,还是 Last-Modified,都是以工夫点来根据,实践上是不出问题,但却出问题了,所以就有了新的计划

其中强缓存存在时,浏览器会采纳强缓存标识符来缓存,当将强缓存设置为生效时,浏览器则会采纳协商缓存来做缓存策略

以上,即便笔者所了解的 HTTP 缓存

附上 demo 地址

参考资料

  • 深刻了解浏览器的缓存机制
  • 彻底了解浏览器的缓存机制
  • 前端缓存最佳实际
  • 浏览器缓存的力量
  • node 实际彻底搞懂强缓存和协商缓存
  • 浅析 HTTP 缓存
  • MDN web docs
  • 图解 HTTP 缓存
退出移动版