记录浏览器缓存

594 阅读9分钟

记录自己所学,直接进入主题。

强缓存

Expires和Cache-Control
Expires: Wed, 22 Nov 2019 08:41:00 GMT

Expires即过期时间,http1.0使用的字段;告诉浏览器在这个时间之前可以直接从缓存里边获取数据,无需再次请求;就比如上边的,表示资源在2019年11月22号8点41分过期,过期之后就需要重新请求资源;

这个方式的问题在于,服务器时间和客户端时间可能不一致,导致缓存混乱。因此这种方式在HTTP/1.1中被抛弃了

Cache-Control

在HTTP/1.1中采取了一个非常关键的字段Cache-Control,采用过期时长来控制缓存,对应的字段是max-age.比如这个例子:

Cache-Control:max-age=3600

它采用过期时长来控制缓存,表示一个小时内可以直接使用缓存,

它其实可以组合非常多的指令,完成更多场景的内存判断,将一些关键的属性列举如下:

  • public 客户端和代理服务器都可以缓存;
  • private 只有浏览器能够缓存了,中间代理服务器不能缓存
  • no-cache 跳过当前的强缓存,发送HTTP请求,即直接进入协商缓存阶段
  • no-store 非常粗暴,不进行任何形式的缓存
  • s-maxage 和max-age长的很像,区别在于是代理服务器的缓存时间

值得注意的是,当Expires和Cache-Control同时存在的时候,Cache-Control会优先考虑。

当然还有另外一种情况,当资源缓存时间超时了,也就是强缓存失效了,这样就进入到第二级屏障———协商缓存了;

协商缓存

Last-Modified

即最后修改时间。在浏览器第一次向服务器发送请求时,会在响应头中加入这个字段。

浏览器接收到后,如果再次请求会在请求头中携带If-Modified-Since这个字段,这个字段的值就是服务器返回的Last-Modified的值

服务器拿到If-Modified-Since这个字段的值和服务器相应资源最后修改时间对比:

  • 如果请求头中的这个值小于最后修改时间,证明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
  • 否则返回304,告诉浏览器直接使用缓存
ETag

ETag是服务器根据当前文件的内容,给文件生成唯一标识。只要文件有改动,这个值就会变。服务器通过响应头把这个值传给浏览器;

浏览器在接收到ETag这个值后,会在下次请求,把这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。

服务器收到If-None-Match这个值后,会和ETag这个值做对比:

  • 如果相同,则返回304,直接使用缓存。
  • 否则,说明要更新了。返回新的资源,跟常规的HTPP请求响应的流程一样。
两者对比
  1. 在精度上ETag,优于Last-Modified,ETag是按照内容给资源上标识,因此能准确感知资源的变化。而Last-Modified在一些特殊的情况并不能准确感知资源变化,主要有两种情况:
  • 编辑了文件资源,但是文件内容没有更改,这样也会造成缓存失效
  • Last-Modified的感知单位是秒,如果内容改动小于1秒则不能准确感知。
  1. 在性能上Last-Modified优于ETag,每一次文件改动ETag都要根绝文件的内容生成哈希值,而Last-Modified仅仅只是记录一个时间点;

如果Last-Modified和ETag同时存在,服务器会优先考虑ETag;

缓存位置

前边强缓存我们说到,如果缓存命中我们直接从内存中读取资源,那这些资源又存储在什么位置呢?

浏览器中缓存位置一共四种,按优先级从高到低排列依次是:

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache
Service Worker

Service Worker借鉴了Web Worker的思路,即让JS运行在主线程之外,因为脱离了浏览器的窗体,因此无法直接访问DOM;即使如此,依然可以帮助我们完成很多有用的功能,比如:离线缓存 消息推送 和 网络代理等功能;其中的 离线缓存 就是 Service Worker Cache.

Memory Cache和Disk Cache

Memory Cache指的是内存缓存,从效率上来讲是最快的。从存活时间来说又是最短的,当渲染进程结束后,内存缓存也就不存在了。

Disk Cache就是磁盘中的缓存,存取效率比内存慢,优势在于存储容量和时长。

既然两者各有优劣,浏览器缓存位置该怎么选择呢?

  • 比较大的js css文件放进磁盘,反之丢进内存
  • 内存使用率比较高的时候,文件优先进入磁盘
Push Cache

即推送缓存,这是浏览器最后一道防线。他是HTTP/2的内容,随着HTTP/2的推广,它的应用会越来越广泛,关于Push Cache请参考扩展文章

总结

先对浏览器缓存做个总结 首先通过Cache-Control验证强缓存是否命中,如果命中直接使用缓存,否则进入协商缓存通过请求头中的 If-Modified-SinceIf-None-Match来判断是否命中

  • 命中,返回304状态码,告诉浏览器从缓存中读取资源
  • 否则,返回200状态码,资源更新。

实践

建一个小demo,项目目录

image.png

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>前端缓存</title>
</head>
<style>
    .image_box{
        width: 800px;
        margin: 200px auto;
    }
</style>
<body>
    <div class="image_box"><img style="width: 100%;" src="./images/cat.jpeg" /></div>
</body>
</html>

.babelrc

{
    "presets": [
        [
            "@babel/preset-env",
                {
                "targets": {
                    "node": "current"
                }
            }
        ]
    ]
}

index.js

require('@babel/register');
require('./server.js');

package.json

{
    "name": "webcache",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "server": "nodemon ./index.js"
    },
    "author": "webfansplz",
    "license": "MIT",
    "devDependencies": {
        "@babel/core": "^7.2.2",
        "@babel/preset-env": "^7.2.3",
        "@babel/register": "^7.0.0",
        "koa": "^2.6.2",
        "koa-conditional-get": "^3.0.0",
        "koa-etag": "^4.0.0",
        "koa-static": "^5.0.0"
    },
    "dependencies": {
        "koa-router": "^10.1.1",
        "nodemon": "^1.18.9"
    }
}

server.js

import Koa from 'koa';
import path from 'path';
//静态资源中间件
import resource from 'koa-static';
const app = new Koa();
import conditional from 'koa-conditional-get';
import etag from 'koa-etag';
const host = 'localhost';
const port = 3000;

// etag works together with conditional-get
// app.use(conditional());
// app.use(etag());

app.use(resource(
    path.join(__dirname, './static'),
    {
        // maxage: 10*1000
    }
));

app.listen(port, () => {
    console.log(`server is listen in ${host}:${port}`);
});

接下来,npm run server启动

image.png

浏览器 localhost:3000打开页面

验证强缓存

打开图片的请求我们看到,

image.png

Cache-Control:max-age=0

默认不缓存,我们将max-age设置为10s,再来请求,放开server.js中的注释

maxage: 10*1000

刷新页面,

image.png

可以看到设置max-age=10,后改为从 内存中读取,说明设置强缓存成功了,那么Cache-Control,是什么时候加上去的呢,

koa-static源码中引入了koa-send库。截取部分koa-send源码,只要传入maxage,就会设置Cahche-Controlmax-age;

image.png

另外我们发现

image.png

也就是html强缓存无效每次都会重新请求,

image.png

可以看到请求头中每次浏览器都强制加上了Cache-Control:max-age=0,这可能是保证每次拿到最新的资源。

验证协商缓存
  1. Last-Modified/If-Modified-Since

image.png

不知大家注意到没,响应头中已经包含了Last-Modified这个字段

image.png

Last-Modified是什么时候加上去的呢,截取koa-send部分源码可以看到,

fe5f605c9715ce55d8fabeae037e6fd.png

响应头中如果没有Last-Modified这个字段就添加上去,注意:这里只是针对静态资源,并不是所有的响应头都会加上这个字段,比如ajax请求就没有这个字段,下边会展开说。

image.png

请求和响应中的时间是一致的,为什么没有命中协商缓存呢,放开

app.use(conditional());

再来刷新下页面,

image.png

返回状态码304,命中了协商缓存,那么 conditional()做了什么,截取源码,

image.png

可以看到查看请求的新鲜度(下边会说到),如果新鲜可用,就返回304状态码。

偷偷更换下图片,

image.png

换过图片之后If-Modified-Since的值小于Last-Modified中的值,可以看到200 OK的状态码,已经重新请求了。

  1. ETag/If-None-Match

怎样给请求加上呢,使用koa-etag这个模块,放开

app.use(etag());

同时我们注消掉Last-modified,

image.png

image.png

返回和请求中都已携带ETag,且没有Last-Modified/If-Modified-Since的干扰,ETag中的值和If-None-Match中的值相同,命中缓存返回304.

接下来改变图片,重新请求

image.png

看到ETag中的值和If-None-Match中的值不同,重新请求。和Last-Modified/If-Modified-Since逻辑基本上是一样的,条件不同。

新鲜度检测

  1. koa-conditional-get 上边看到 koa-conditional-get可以让缓存生效,返回304,body为null,

image.png

  1. 在koa模块request.js中看到

image.png 表示200到300的状态码和304就行行新鲜度检测,

  1. fresh 主要代码
function fresh (reqHeaders, resHeaders) {
  // 分别获取 if-modified-since if-none-match的值
  var modifiedSince = reqHeaders['if-modified-since']
  var noneMatch = reqHeaders['if-none-match']

  // 如果两个都没有直接放回 false 表示不新鲜 重新请求
  if (!modifiedSince && !noneMatch) {
    return false
  }

  // 2. 给端对端测试用的,因为浏览器的Cache-Control: no-cache请求
  //    是不会带if条件的 不会走到这个逻辑
  var cacheControl = reqHeaders['cache-control']
  if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
    return false
  }

  // 比较 ETag 和 if-none-match
  if (noneMatch && noneMatch !== '*') {
    var etag = resHeaders['etag']

    if (!etag) {
      return false
    }

    var etagStale = true
    var matches = parseTokenList(noneMatch)
    for (var i = 0; i < matches.length; i++) {
      var match = matches[i]
      if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
        etagStale = false
        break
      }
    }

    if (etagStale) {
      return false
    }
  }

  // 比较 ETag 和 if-modified-since
  if (modifiedSince) {
    var lastModified = resHeaders['last-modified']
    var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))

    if (modifiedStale) {
      return false
    }
  }

  return true
}

ajax请求的缓存

首先要明白ajax请求需要我们手动设置响应头Cache-Control,Last-Modified,ETag,要根据业务实际场景去定义:比如说max-age,Etag的生成方式,生成这些之后,浏览器会自动帮我们带上这些请求头,conditional会进行协商缓存的控制,是否会返回304.

总结

  1. 发出请求后,会现在本地查找缓存
  2. 请求首先验证强缓存Cache-Control是否命中,max-age是否过期
  3. 过期重新请求和缓存到本地,命中直接置用缓存
  4. 强缓存过期后进入协商缓存,首先对比ETag字段是否一致,一致,返回304使用本地缓存
  5. 不一致,重新获取数据返回200
  6. 没有ETag,对比Last-Modified字段,和ETag同理。

对于前端来说,我们能做的其实很有限,因为关于缓存的响应头主要是后端来控制,浏览器会自动携带响应头中关于缓存的字段,当然我们也可根据实际场景自定义携带缓存相关请求头,比如说:no-cache等.

参考文章

  1. 轻松理解浏览器缓存(Koa缓存源码解析)
  2. 实践这一次,彻底搞懂浏览器缓存机制
  3. (1.6w字)浏览器灵魂之问,请问你能接得住几个?