浏览器缓存一探究竟~

img

先看一张经典的流程图,结合理解html

吃了它~前端

img

1. 缓存做用

  • 减小了冗余的数据传输,节省了网费。
  • 减小了服务器的负担, 大大提升了网站的性能
  • 加快了客户端加载网页的速度

2. 缓存分类

2.1 DNS 缓存

主要就是在浏览器本地把对应的 IP 和域名关联起来,这样在进行 DNS 解析的时候就很快。git

2.2 MemoryCache

是指存在内存中的缓存。从优先级上来讲,它是浏览器最早尝试去命中的一种缓存。从效率上来讲,它是响应速度最快的一种缓存。 内存缓存是快的,也是“短命”的。它和渲染进程“生死相依”,当进程结束后,也就是 tab 关闭之后,内存里的数据也将不复存在。github

2.3 浏览器缓存

浏览器缓存,也称Http 缓存,分为强缓存协商缓存。优先级较高的是强缓存,在命中强缓存失败的状况下,才会走协商缓存浏览器

2.3.1 强缓存

强缓存是利用 http 头中的 ExpiresCache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 Expirescache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通讯。缓存

Expires

实现强缓存,过去咱们一直用Expires。当服务器返回响应时,在 Response Headers 中将过时时间写入 Expires 字段。像这样服务器

expires: Wed, 12 Sep 2019 06:12:18 GMTmarkdown

能够看到,expires 是一个时间戳,接下来若是咱们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,若是本地时间小于 expires 设定的过时时间,那么就直接去缓存中取这个资源。网络

从这样的描述中你们也不难猜想,expires 是有问题的,它最大的问题在于对本地时间的依赖。若是服务端和客户端的时间设置可能不一样,或者我直接手动去把客户端的时间改掉,那么 expires 将没法达到咱们的预期。session

Cache-Control

考虑到 expires 的局限性,HTTP1.1 新增了Cache-Control字段来完成 expires 的任务。expires 能作的事情,Cache-Control 都能作;expires 完成不了的事情,Cache-Control 也能作。所以,Cache-Control 能够视做是 expires 的彻底替代方案。在当下的前端实践里,咱们继续使用 expires 的惟一目的就是向下兼容。

Cache-Control 中,咱们经过max-age来控制资源的有效期。max-age 不是一个时间戳,而是一个时间长度。在本例中,max-age31536000 秒,它意味着该资源在 31536000 秒之内都是有效的,完美地规避了时间戳带来的潜在问题。

Cache-Control 相对于 expires 更加准确,它的优先级也更高。当 Cache-Control 与 expires 同时出现时,咱们以 Cache-Control 为准。

能够参考下下面两张图:

img

2.3.2 协商缓存(对比缓存)

协商缓存依赖于服务端与浏览器之间的通讯。协商缓存机制下,浏览器须要向服务器去询问缓存的相关信息,进而判断是从新发起请求、下载完整的响应,仍是从本地获取缓存的资源。若是服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种状况下网络请求对应的状态码是 304。

协商缓存的实现,从 Last-ModifiedEtag,Last-Modified 是一个时间戳,若是咱们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:

Last-Modified

Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT
复制代码

随后咱们每次请求时,浏览器的请求头 headers 会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 Last-Modified 值:

If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT
复制代码

服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。若是发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;不然,返回 304 响应,Response Headers 不会再添加 Last-Modified 字段。

以下图:

img

经过最后修改时间来判断缓存是否可用

  • Last-Modified:响应时告诉客户端此资源的最后修改时间
  • If-Modified-Since:当资源过时时(使用 Cache-Control 标识的 max-age),发现资源具备 Last-Modified 声明,则再次向服务器请求时带上头 If-Modified-Since
  • 服务器收到请求后发现有头 If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应最新的资源内容并返回 200 状态码;
  • 若最后修改时间和 If-Modified-Since 同样,说明资源没有修改,则响应 304 表示未更新,告知浏览器继续使用所保存的缓存文件。

看个实例代码:

let http = require('http');
let fs = require('fs');
let path = require('path');
let mime = require('mime');
http.createServer(function (req, res) {
    let file = path.join(__dirname, req.url);
    fs.stat(file, (err, stat) => {
        if (err) {
            sendError(err, req, res, file, stat);
        } else {
            let ifModifiedSince = req.headers['if-modified-since'];
            if (ifModifiedSince) {
                if (ifModifiedSince == stat.ctime.toGMTString()) {
                    res.writeHead(304);
                    res.end();
                } else {
                    send(req, res, file, stat);
                }
            } else {
                send(req, res, file, stat);
            }
        }
    });
}).listen(8080);
function send(req, res, file, stat) {
    res.setHeader('Last-Modified', stat.ctime.toGMTString());
    res.writeHead(200, { 'Content-Type': mime.getType(file) });
    fs.createReadStream(file).pipe(res);
}
function sendError(err, req, res, file, stat) {
    res.writeHead(400, { "Content-Type": 'text/html' });
    res.end(err ? err.toString() : "Not Found");
复制代码

使用 Last-Modified 存在一些弊端,这其中最多见的就是这样几个场景 1. 某些服务器不能精确获得文件的最后修改时间, 这样就没法经过最后修改时间来判断文件是否更新了。 2. 咱们编辑了文件,但文件的内容没有改变。服务端并不清楚咱们是否真正改变了文件,它仍然经过最后编辑时间进行判断。所以这个资源在再次被请求时,会被当作新资源,进而引起一次完整的响应——不应从新请求的时候,也会从新请求。 3. 当咱们修改文件的速度过快时(好比花了 100ms 完成了改动),因为 If-Modified-Since 只能检查到以为最小计量单位的时间差,因此它是感知不到这个改动的——该从新请求的时候,反而没有从新请求了。 4. 若是一样的一个文件位于多个CDN服务器上的时候内容虽然同样,修改时间不同。

第二和第三这两个场景其实指向了同一个 bug——服务器并无正确感知文件的变化。为了解决这样的问题,Etag 做为 Last-Modified 的补充出现了。

Etag

这个是协商缓存中的另一种

Etag 是由服务器为每一个资源生成的惟一的标识字符串(指纹),这个标识字符串能够是基于文件内容编码的,只要文件内容不一样,它们对应的 Etag 就是不一样的,反之亦然。所以 Etag 可以精准地感知文件的变化。

Etag是 Web 服务端产生的,而后发给浏览器客户端。生成过程须要服务器额外付出开销,会影响服务端的性能,这是它的弊端。所以启用 Etag 须要咱们审时度势。正如咱们刚刚所提到的——Etag 并不能替代 Last-Modified,它只能做为 Last-Modified 的补充和强化存在。

执行流程是这样的: 1. 客户端想判断缓存是否可用能够先获取缓存中文档的ETag,而后经过If-None-Match发送请求给 Web 服务器询问此缓存是否可用。 2. 服务器收到请求,将服务器的中此文件的ETag,跟请求头中的If-None-Match相比较,若是值是同样的,说明缓存仍是最新的,Web 服务器将发送304 Not Modified响应码给客户端表示缓存未修改过,可使用。 3. 若是不同则 Web 服务器将发送该文档的最新版本给浏览器客户端

看以下实例代码:

let http = require("http");
let fs = require("fs");
let path = require("path");
let mime = require("mime");
let crypto = require("crypto");
http
  .createServer(function(req, res) {
    let file = path.join(__dirname, req.url);
    fs.stat(file, (err, stat) => {
      if (err) {
        sendError(err, req, res, file, stat);
      } else {
        let ifNoneMatch = req.headers["if-none-match"];
        let etag = crypto
          .createHash("sha1")
          .update(stat.ctime.toGMTString() + stat.size)
          .digest("hex");
        if (ifNoneMatch) {
          if (ifNoneMatch == etag) {
            res.writeHead(304);
            res.end();
          } else {
            send(req, res, file, etag);
          }
        } else {
          send(req, res, file, etag);
        }
      }
    });
  })
  .listen(8080);
function send(req, res, file, etag) {
  res.setHeader("ETag", etag);
  res.writeHead(200, { "Content-Type": mime.lookup(file) });
  fs.createReadStream(file).pipe(res);
}
function sendError(err, req, res, file, etag) {
  res.writeHead(400, { "Content-Type": "text/html" });
  res.end(err ? err.toString() : "Not Found");
}
复制代码

强缓存和协商缓存比较

优先级:

Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 EtagLast-Modified 同时存在时,以 Etag 为准。

对比:

  • 强制缓存若是生效,不须要再和服务器发生交互,而对比缓存不论是否生效,都须要与服务端发生交互
  • 两类缓存规则能够同时存在,强制缓存优先级高于对比缓存,也就是说,当执行强制缓存的规则时,若是缓存生效,直接使用缓存,再也不执行对比缓存规则

2.4 Service Worker Cache

Service Worker 是一种独立于主线程以外的 Javascript 线程。它脱离于浏览器窗体,所以没法直接访问 DOM。这样独立的个性使得 Service Worker 的“我的行为”没法干扰页面的性能,这个幕后工做者能够帮咱们实现离线缓存消息推送网络代理等功能。咱们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。

Service Worker 的生命周期包括 install、activited、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非咱们主动终止它。这是它能够用来实现离线存储的重要先决条件.

它就在浏览器开发工具(F12) Application 标签页中

2.5 Push Cache

Push Cache 是指 HTTP2server push 阶段存在的缓存。这块的知识比较新,应用也还处于萌芽阶段,应用范围有限不表明不重要——HTTP2 是趋势、是将来。在它还未被推而广之的此时此刻,仍但愿你们能对 Push Cache 的关键特性有所了解:

  • Push Cache 是缓存的最后一道防线。浏览器只有在 Memory CacheHTTP CacheService Worker Cache 均未命中的状况下才会去询问 Push Cache
  • Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
  • 不一样的页面只要共享了同一个 HTTP2 链接,那么它们就能够共享同一个 Push Cache

3. 请求流程

3.1 第一次请求

img

3.2 第二次请求

走上面的缓存机制

4. 如何干脆不发请求

  • 浏览器会将文件缓存到Cache目录,第二次请求时浏览器会先检查Cache目录下是否含有该文件,若是有,而且还没到Expires设置的时间,即文件尚未过时,那么此时浏览器将直接从 Cache 目录中读取文件,而再也不发送请求
  • Expires是服务器响应消息头字段,在响应 http 请求时告诉浏览器在过时时间前浏览器能够直接从浏览器缓存取数据,而无需再次请求,这是HTTP1.0的内容,如今浏览器均默认使用HTTP1.1,因此基本能够忽略
  • Cache-ControlExpires的做用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据仍是从新发请求到服务器取数据,若是同时设置的话,其优先级高于Expires

5.1 使用 Cache-Control

  • private 客户端能够缓存
  • public 客户端和代理服务器均可以缓存
  • max-age=60 缓存内容将在 60 秒后失效
  • no-cache 须要使用对比缓存验证数据,强制向源服务器再次验证
  • no-store 全部内容都不会缓存,强制缓存对比缓存都不会触发
  • Cache-Control:private, max-age=60, no-cache
let http = require("http");
let fs = require("fs");
let path = require("path");
let mime = require("mime");
let crypto = require("crypto");
http
  .createServer(function(req, res) {
    let file = path.join(__dirname, req.url);
    console.log(file);

    fs.stat(file, (err, stat) => {
      if (err) {
        sendError(err, req, res, file, stat);
      } else {
        send(req, res, file);
      }
    });
  })
  .listen(8080);
function send(req, res, file) {
  let expires = new Date(Date.now() + 60 * 1000);
  res.setHeader("Expires", expires.toUTCString());
  res.setHeader("Cache-Control", "max-age=60");
  res.writeHead(200, { "Content-Type": mime.lookup(file) });
  fs.createReadStream(file).pipe(res);
}
function sendError(err, req, res, file, etag) {
  res.writeHead(400, { "Content-Type": "text/html" });
  res.end(err ? err.toString() : "Not Found");
}
复制代码

参考资料

谈谈浏览器缓存

最后

若是本文对你有帮助的话,给本文点个赞吧

鄙人github,一块儿学习~

相关文章
相关标签/搜索