浏览器缓存的相关知识介绍

2,373 阅读16分钟

引言

开发者来说,浏览器充当了重要角色。浏览器缓存(Brower Caching)是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档。浏览器缓存的优点有: (1)、减少了冗余的数据传输,节省了网费, (2)、减少了服务器的负担,大大提升了网站的性能, (3)、加快了客户端加载网页的速度

浏览器缓存规则

强制缓存

不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码,并且size显示from disk cache或from memory cache。from memory cache代表使用内存中的缓存,from disk cache则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为memory –> disk。

描述

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况: •协商缓存生效,返回304和Not Modified 协商缓存生效 •协商缓存失效,返回200和请求结果 协商缓存失效

描述

举例来说, C:S,你几岁了? S:C,我18岁了。 C:S,你几岁了?我猜你18岁了。 S:你知道还问我?(304) C:S,你几岁了?我猜你18岁了。 S:C,我19岁了。(200)

描述

Respouse Headers的设置与缓存的关系

Respouse Headers缓存相关请求头与说明

描述

下面通过demo,进行了了验证(由于第一次请求都是需要从服务器端获取数据,返回200,所以就不再贴验证图了) last-Modified

第二次请求数据未更改时:

描述

第二次请求数据更改:

客户端第二次请求此URL时,根据HTTP协议的规定,浏览器会向服务器传送If-Modified-Since报头(HttpRequest Header),询问该时间之后文件是否有被修改过:如果服务器端的资源没有变化,则自动返回HTTP304(NotChanged.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。

2、ETag工作原理

第二次发送请求:

(1)数据没有变化

描述

服务端通过对比etag与if-none-match,数据未发生变化时,etag不会发生变化,服务端检测两者相等,返回304

(2) 数据发生变化

数据发生变化时,etag发生变化,服务端检测两者不相等,返回304,返回200

3、 Expires

第二次请求(在有效时间内):

描述

第二次请求(不在有效时间内):

描述

当请求在有效时间类,客户端会直接读取浏览器缓存,返回200(from disk cache),当不在有效时间内时,会进行协商缓存,返回304. 4、 Cache-Control

(在HTTP/1.0中可能部分没实现,仅仅实现了Pragma: no-cache),HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存。比如当Cache-Control:max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。常见有以下六个属性值:

描述
我们主要对常用的相关属性进行了验证,包括max-age,no-cache,no-store

(1) max-age

当max-age=0时,表示浏览器不会进行缓存,如下图所示:

第二次请求:

描述

当max-age = 60,表示浏览器缓存60s

第二次请求(60秒以内):

描述

第二次请求(60秒以外):

描述

由此可看出,在60s以内,浏览器会直接从缓存中获取数据,超过60s,浏览器会重新从服务器端获取数据。

各请求头之间的关系

Cache-Control的no-cache与Etag或Last-Modified

no-cache控制客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。我们具体以实例来理解:

if (req.headers["if-modified-since"] === data) {
        res.writeHead(304, "keep cache", {
            "Content-Type": "text/plain",
            "Cache-Control": "no-cache",
            "last-modified": data,
            //"Expires": datae
        })
        res.end();
} else {
    res.writeHead(200, "OK", {
        "Content-Type": "text/plain",
        "Cache-Control": "no-cache",
        "last-modified": data,
        // "Expires": datae
    })
}

第二次请求数据无变化:

描述

第二次请求数据发生变化:

描述

描述

当Cache-Control设为no-cache时,需要使用协商缓存判断是否使用缓存,当第二次访问时,将Last-Modified与 if-modified-since进行比较,若相同,则使用协商缓存,返回304,若不同,则重新访问服务端,返回200。no-cache与Etag和Last-Modified是一样的原理,这里就不再实验。

Cache-Control的no-store与Etag或Last-Modified

no-store即所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存。

        if (req.headers["if-modified-since"] === data) {
            res.writeHead(304, "keep cache", {
                "Content-Type": "text/plain",
                "Cache-Control": "no-store",
                "last-modified": data,
                //"Expires": datae
                "Etag": number
            })
            res.end();
        } else {
            res.writeHead(200, "OK", {
                "Content-Type": "text/plain",
                "Cache-Control": "no-store",
                "last-modified": data,
               // "Expires": datae
                "Etag": number
        })

第二次请求:

描述

描述

说明,当catch-control设为no-store时,浏览器不会在本地缓存数据,客户端每次访问都会向服务器请求数据。

关于 Cache-Control: max-age=秒 和 Expires

Expires = 时间,HTTP 1.0 版本,缓存的载止时间,允许客户端在这个时间之前不去检查(发请求) max-age = 秒,HTTP 1.1版本,资源在本地缓存多少秒。 如果max-age和Expires同时存在,则被Cache-Control的max-age覆盖。

Expires 的一个缺点就是,返回的到期时间是服务器端的时间,这样存在一个问题,如果客户端的时间与服务器的时间相差很大,那么误差就很大,所以在HTTP 1.1版开始,使用Cache-Control: max-age=秒替代。

res.writeHead(200, "keep cache", {
    "Content-Type": "text/plain",
    "Cache-Control": "max-age=60",
    //"last-modified": data,
    "Expires": datae
    // "Etag": number
})

第二次请求(60s之后):

描述

描述

可以看出,虽然Expires的期限未至,但max-age=60s,60s后,浏览器便会重新访问服务器,很明显,如果max-age和Expires同时存在,则被Cache-Control的max-age覆盖。

关于 Cache-Control: max-age=秒 和Etag或Last-Modified

猜想:当max-age过期后,是否会判断etag或Last-Modified,进行协商缓存了?

我们以事实来说话:

    if (req.headers["if-modified-since"] === data) {
            res.writeHead(304, "keep cache", {
                "Content-Type": "text/plain",
                "Cache-Control": "max-age=60",
                "last-modified": data,
                //"Expires": datae
               // "Etag": number
            })
            res.end();
        } else {
          res.writeHead(200, "OK", {
              "Content-Type": "text/plain",
              "Cache-Control": "max-age=60",
              "last-modified": data,
              //"Expires": datae
              // "Etag": number
        })
      }

第二次请求(60s以内):

描述

第二次请求,数据未发生变化(60s以外):

描述

第二次请求,当数据发生变化时(60s外):

描述

描述

由此我们可以得到,当max-age的时间过期后,浏览器会进入协商缓存,通过比较last-modify与if-modified-since,若相等,则返回304,从缓存中读取数据,或不相等,返回200,从服务端读取数据。Etag原理与last-modify一样,这里就不再重复。

5、Cache-Control 与last-modify与Etag

讲到这里,可能我们会有个疑问,感觉etag与last-modify的原理一样,作用一样,为什么还会有两个了。我们就分析下这两者的区别: last-modify的缺点: 一些周期性修改的文件,修改时间变了但内容没变,此时不希望重新GET; 一些文件修改非常频繁,比如1秒内修改了多次,Last-Modified只能精确到秒; 一些服务器不能得到文件修改的精确时间; ETag是HTTP/1.1标准开始引入的,它是对Last-Modified的补充。 当etag与last-modify同时存在时,如果同时有 etag 和 last-modified 存在,在发送请求的时候会一次性的发送给服务器,没有优先级,服务器会比较这两个信息(在具体实现上,大多数做法针对这种情况只会比对 etag)。 我们还是通过实例来看具体情况:

        if (req.headers["if-none-match"] == number) {
            if (req.headers["if-modified-since"] === data){
                res.writeHead(304, "keep cache", {
                    "Content-Type": "text/plain",
                   // "Cache-Control": "no-cache",
                   "Cache-Control": "max-age=60",
                    "last-modified": data,
                   // "Expires": datae,
                    "Etag": number
                })
                res.end();
            }
        } else {
            res.writeHead(200, "OK", {
                "Content-Type": "text/plain",
                "Cache-Control": "no-cache",
                "last-modified": data,
                //"Expires": datae
                "Etag": number
            })
        }

第二次访问数据无变化:

描述

第二次访问last-modify有变化:

描述

第二次访问etag变化:

描述

描述

同时改变就不演示了,其实大多数情况下,当我们内容改变时,通过相关插件监控,last-modify和etag都会改变,这样就会返回200,从服务器端读取数据。

6 、Cache-Control 与last-modify与Etag与Expires

通常Cache-Control 与 Last-Modified, Etag, Expire 是一起混合使用的,特别是 Last-Modified 和 Expire 经常一起使用。Last-Modified,Etag,Expires 三个同时使用时。先判断 Expire ,然后发送 Http 请求,服务器同时判断 last-modified和 Etag ,必须都没有过期,才能返回 304 响应。 Cache-Control —— 请求服务器之前 Expires —— 请求服务器之前 If-None-Match (Etag) —— 请求服务器 If-Modified-Since (Last-Modified) —— 请求服务器 我们同样通过实例来看:

(1)我们将cache-control设为no-cache,允许使用协商缓存,Expires设为2020年,保证不会到期,然后看下情况:

    if (req.headers["if-none-match"] == number) {
        if (req.headers["if-modified-since"] === data){
            res.writeHead(304, "keep cache", {
                "Content-Type": "text/plain",
                // "Cache-Control": "no-cache",
                "Cache-Control": "max-age=60",
                "last-modified": data,
                "Expires": datae,
                "Etag": number
            })
            res.end();
        }
    } else {
        res.writeHead(200, "OK", {
            "Content-Type": "text/plain",
            // "Cache-Control": "no-cache",
            "Cache-Control": "max-age=60",
            "last-modified": data,
            "Expires": datae,
            "Etag": number
        })
    }

第二次请求,数据不发生变化:

描述

描述

其实这里,如果只有expires时,应该不会向服务端发起请求,会出现200(from disk cache).当设置了cache-control为no-cache,会覆盖expires,表示需要进入协商缓存这一环节,若检测到数据没变化,则返回304.

(2) 我们将cache-control设为max-age=60,允许使用协商缓存,Expires设为2020年,保证不会到期,

然后看下情况:

第二次请求(60s以内)

描述

由于还在max-age期限内,所以会直接读取缓存,返回200(from disk cache)

第二次请求(60s以后),数据未变化:

描述

第二次请求(60s以后)当数据发生变化时:

描述

描述

60s以后,由于max-age过期,浏览器会向服务器发送信息,若数据未变化,返回304,若变化返回200和数据,由于请求了服务器,max-age会重新计时,又进入强制缓存。这里可以看出,expires与max-age功能一样,为什么还有会两个,其实expires是HTTP1.0中使用的,Expires要求客户端和服务端的时钟严格同步,HTTP1.1中引入Cache-Control来克服Expires头的限制。

讲到这里,其实它们之间的关系已经很明了了,做个总结:

Cache-Control —— 请求服务器之前 Expires —— 请求服务器之前 If-None-Match (Etag) —— 请求服务器 If-Modified-Since (Last-Modified) —— 请求服务器 当cache-control的max-age与Expires时间未过期时,不会向服务器发送请求,使用强缓存。 当cache-control设置为no-cache时,会使用协商缓存。 当cache-control设置为no-store时,不使用缓存,每次都会向服务器发送请求。 Cache-control是克服expires缺陷,HTTP1.1引入,优先级高于expires。 ETag是HTTP/1.1标准开始引入的,它是对Last-Modified的补充。

浏览器缓存与前端性能优化

首先分别对无缓存,协商缓存,强缓存进行9次请求,得出各自耗费的时间。很明显,强缓存所耗时间是最短的。无缓存即每次都需要发送http请求,服务端返回200和数据,协商缓存需要发送一次http请求,服务端进行判断,数据未变返回304,客户端读取浏览器缓存,变化返回200和数据,而强缓存是不发送http请求,客户端直接读取浏览器缓存。所以浏览器缓存与前端性能就主要围绕在是否发送http请求或发送http请求多少来进行优化了。 那么减少http请求有哪些方式了?

合并图片

CSSSprites直译过来就是CSS精灵,但是这种翻译显然是不够的,其实就是通过将多个图片融合到一副图里面,然后通过CSS的一些技术布局到网页上。特别是图片特别多的网站,如果能用css sprites降低图片数量,带来的将是速度的提升。

合并css/js

因为我们在浏览器中打开一个页面,页面中所需要用到的每一个css,js文件,     客户端都需要发送一次http请求到服务器端获取这些文件,所以在页面的设计时将浏览器一次访问需要的js,css合并成一个文件,这样可以减少http请求来提高网站性能。

充分利用强缓存

对于CSS,Javascript,Logo,图标等这些静态资源文件更新的频率都比较低,所以将这些静态资源文件使用强缓存,这样就可以减少http请求,从而提高性能。页面的初次访问者会进行很多HTTP请求,我们通过设置Expires头和Cache-Control的max-age,是这些组件能够缓存足够长的时间,这样第二次请求时就直接读取浏览器缓存,从而避免发送http请求,加载速度就会提高很多。 但是通过使用一个长久的Expires头,可以使这些组件被缓存,下次访问的时候,就可以减少不必要的HTPP请求,从而提高加载速度。在某些时候,静态资源文件变换需要更新到客户端的浏览器缓存中,这种情况下可以通过改变文件的文件名来实现,即使用浏览器缓存的网站如果需要更新CSS,JS,Logo等信息,可以通过改变文件名的方式进行更新。当我们的文件跟新时,我们最希望的是我们更改了哪里,对应的部分进行更新。这里我们就引入数据摘要要算法对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据。目前成熟的解决方案:

FIS的解决方案

现在已经跟新到fis3了,fis3中封装工具方法,我们调用fis3进行版本发布,就会自动实现上述部署。 我们只需要在fis-confid.js中配置

fis.match('*.{js,css,png}', {
   useHash: true
});

这样文件会在打包后的发布版本对应的文件添加唯一的hash值 可以看出,js/css/png等都被加上了md5 hash值,这样当我们版本中其中一个文件变化,hash才会变,从而保证浏览器可以读取更改的文件。Fis3跟webpack一样是个复杂的打包工具,这只是其中一个小部分。

webpack的解决方案

webpack通过对不同文件进行hash/chunkhash/contenthash的设置 对png|jpe?g|gif等的设置:

      {
            test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('media/[name].[hash:7].[ext]')
        }
      }

对js的设置:

output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },

对css的设置:

filename: utils.assetsPath('css/[name].[contenthash].css'),
allChunks: true,

当我们打包时,对应的文件会有唯一标志符:

当有文件更改后,对应的hash值就会变化,浏览器就会向服务器重新请求该对应更改文件。

压缩组件

从HTTP1.1开始,Web客户端可以通过HTTP请求中的Accept-Encoding头来表示对压缩的支持Accept-Encoding: gzip,deflate.如果Web服务器看到请求中有这个头,就会使用客户端列出来的方法中的一种来进行压缩。Web服务器通过响应中的Content-Encoding来通知Web客户端。

下面我们来看看具体实例

Eg1:腾讯网 对于png等静态资源的设置:

通过对max-age的设置,是浏览器缓存时间为600s,600s之后便会使用协商缓存,这里主要通过last-modify进行比较。

对于css的设置:

对于css,腾讯网首先进行了打包处理,然后只设置了60s的缓存,60s之后再次请求便会进入协商缓存。

对于js:

对于js,同样进行了打包处理,然后设置了60s的缓存,60s之后再次请求便会进入协商缓存。

eg2网易首页: 对于png等静态资源:

网易首页将png等静态资源设置缓存时间为262800s,相当于3天左右的时间。在这期间,再次请求都会使用强缓存。

对于css的设置:

对css的设置,缓存时间达到315360000s,相当于10年,夸张了点。

对于js:

对于js的设置,将max-age设置为3600s,之后便会通过判断etag和last-modify进行协商判断,进入协商缓存。

不过,由于一个网站有非常多的png,js,css,所以对于特别的情况,需要特殊的设置。对于很少变化的静态文件,可以设置很长的缓存时间,例如10年。对于常变化的,就可以不舍缓存时间或很短比如60s。合理设置浏览器缓存将会大大提升网页加载速度,提升前端性能。

参考资料

浏览器缓存详解:blog.csdn.net/ywh147/arti…

http: baike.baidu.com/item/http/2…

node中文官网:nodejs.org/zh-cn/