整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

前端性能优化(二)-浏览器缓存机制

览器缓存对于前端一点都不陌生,最常见的就是,新版本上线了,测试却说这怎么还没有变化呢?使用 ctr + F5 强制刷新之后,立马就好了。或者清除浏览器缓存,按住ctr+shift+delete,弹出如图:

我们会发现目前浏览器缓存的图片和文件的大小。或者进入chrome://chrome-urls/找到chrome://cache/ 就可以看到所有缓存的地址列表。对于浏览器缓存,前端对它是又爱又恨,有时想保留,有时想禁掉,所以看看浏览器缓存到底是怎样的?

一、什么是浏览器缓存?

浏览器缓存就是浏览器根据 url 第一次访问网站之后,将网站的 html、css、js、图片等文件复制一份保留到浏览器中,当你二次访问这个 url 的网站时,如果网站没有明确表示有更新时,浏览器直接在缓存中查找内容,不会再次请求网页内容,只有网页明确表示有更新时,浏览器才会向服务器发起网路请求,再次下载网页。

如上图,百度首页就是使用了缓存机制,首次访问之后 web资源被缓存,在后面重复请求中,资源直接在缓存中读取,而不是向服务器请求资源。

二、为什么使用缓存?

2.1、为什么很多网站二次打开速度很快?

网页二次打开很快,主要原因是第一次加载页面过程中,缓存了部分耗时数据,这一现象,对于单页面应用开发非常明显。

上一篇文章《浏览器工作原理》中,浏览器工作流程介绍,输入网址回车以后浏览器向服务器发起服务之前,会现在浏览器缓存中查询是否有需要的文件?如果有则直接在缓存中获取文件,避免向服务器请求和下载文件,所以节省了一部分时间。

2.2、浏览器缓存优点

1、减少网络带宽消耗

对于网站运营者或者访问网页的用户,带宽就代表着 money ,过多的消耗带宽,我们服务器配置就得升级,使用浏览器缓存之后,就会减少网络流量,降低运营成本。

2、降低服务器压力

使用浏览器缓存之后,除第一次访问需要向服务器请求网站全部资源,后续访问可以重复使用浏览器本地缓存,减少对服务器的请求,间接降低服务器的压力,同时,搜索引擎的爬虫也会根据缓存过期机制降低抓取的频率,也可以降低服务器压力。

3、减少网络延迟,加快网页加载

浏览器缓存 web资源后,减少网络请求,可以更快速地获取到服务器返回数据,同时使用浏览器缓存内的文件比服务器获取快很多,所以网页加载速度明显快很多。

三、浏览器的缓存规则

对于浏览器端的缓存来讲,这些规则是在 http 协议和 meta 标签中定义的。分别从两个维度:新鲜度和校验值,规定浏览器是否可以直接使用缓存中的副本,还是直接从服务器获取最新资源。

3.1、新鲜度(过期):浏览器缓存的有效期,缓存必须满足以下两个条件,浏览器才会认为是最新的,可以直接使用。

  • 含有完整的过期时间控制头信息,并在有效期内。
  • 浏览器已经使用过这个副本,并且在会话中已经检查过新鲜度。

3.2、校验值(验证):服务器返回资源的时候,会在响应头信息中带上资源实体标签 Entity Tag,可以用来作为浏览器再次请求过程的校验标识,如果发现校验标识不匹配,说明资源已经被修改过或过期,浏览器需要重新请求资源。

四、如何控制缓存?

缓存规则可以设置在html的meta标签,也可以设置在http协议头内。

4.1、前端 html 中 meta 标签

在 html 页面中加入缓存设置,代码如下:

<meta http-equiv="Pragma" content="no-cache"  />
<!-- Pragma是http1.0版本中给客户端设定缓存方式之一 -->

上边代码,禁止浏览器缓存,浏览器每次访问网页都要去服务器请求。事实这种禁用缓存形式作用有限:

  • 只有IE浏览器才能标识这段 meta 的含义,其他主流浏览器仅认识 “Cache-Control:no-store” 的 meta 标签。
  • 在IE浏览器中,并不一定添加 pragma,但是会让当前网页每次都会向服务器发送请求。

4.2、HTTP协议头

http请求和响应头中,与缓存相关的常见类型:

规则

消息报头

值/示例

类型

作用

新鲜度

Pragma

no-cache

响应

告诉浏览器忽略资源的缓存副本,每次访问都需要去服务器拉取【http1.0中存在的字段,在http1.1已被抛弃,使用Cache-Control替代,但为了做http协议的向下兼容,很多网站依旧会带上这个字段】


Expires

Mon, 15 Aug 2016 03:56:47 GMT

响应

启用缓存和定义缓存时间。告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求【http1.0中存在的字段,该字段所定义的缓存时间是相对服务器上的时间而言的,如果客户端上的时间跟服务器上的时间不一致(特别是用户修改了自己电脑的系统时间),那缓存时间可能就没啥意义了。在HTTP 1.1版开始,使用Cache-Control: max-age=秒替代】


Cache-Control

no-cache

响应

告诉浏览器忽略资源的缓存副本,强制每次请求直接发送给服务器,拉取资源,但不是“不缓存”



no-store

响应

强制缓存在任何情况下都不要保留任何副本



max-age=[秒]

响应

指明缓存副本的有效时长,从请求时间开始到过期时间之间的秒数



public

响应

任何路径的缓存者(本地缓存、代理服务器),可以无条件的缓存该资源



private

响应

只针对单个用户或者实体(不同用户、窗口)缓存资源


Last-Modified

Mon, 15 Aug 2016 03:56:47 GMT

响应

告诉浏览器这个资源最后的修改时间。服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端【只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间】


If-Modified-Since

Mon, 15 Aug 2016 03:56:47 GMT

请求

其值为上次响应头的Last-Modified值,再次向web服务器请求时带上头If-Modified-Since。web服务器收到请求后发现有头If-Modified-Since则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),包括更新Last-Modified的值,HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304(无需请求,节省浏览),告知浏览器继续使用所保存的cache

校验值

ETag

"fd56273325a2114818df4f29a628226d"

响应

告诉浏览器当前资源在服务器的唯一标识符(生成规则由服务器决定)


If-None-Match

"fd56273325a2114818df4f29a628226d"

请求

当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match(Etag的值)。web服务器收到请求后发现有头If-None-Match则与被请求资源的相应校验串进行比对,决定返回200或304

各种类型之间的关系和区别:

  • Cache-Control 与 Expires:它两作用一样,都表明当前资源的有效期,控制浏览器是取缓存还是直接向服务器获取,Cache-Control可以设置的更细致,如果同时设置,它的优先级高于Expires。
  • Last-Modified / ETag 与 Cache-Control / Expires:配置Last-Modified/ETag的情况下,浏览器再次访问URL的资源,还是会发送请求到服务器,询问文件是否已经修改,如果没有,服务器会给浏览器返回304,浏览器直接从本地缓存中取就好了,反之,服务器会直接向浏览器返回数据。Cache-Control / Expires 检测本地缓存是否还在有效期内,在有效期内,直接使用本地缓存,阻止发送请求。如果同时设置,Cache-Control / Expiress 优先级更高。一般情况下,两者配合使用,因为即使服务器设置缓存时间, 当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时Last-Modified/ETag将能够很好利用304,从而减少响应开销。
  • Last-Modified 与 ETag:ETag主要是为了解决Last-Modified比较难解决的问题:1、Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的新鲜度。2、如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存。3、有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形。ETag是服务器自动生成或开发者生成对应资源在服务器的唯一标识符,能够更加精准控制缓存。两者可以一起使用,服务器优先验证ETag,一致时,才会继续比对Last-Mofifed,才决定是否要返回304。

五、不能缓存的请求

并不是所有的请求都能被缓存,无法被缓存的有:

  • post 请求无法被缓存。
  • 需要根据cookie、认证信息等决定输入内容的动态请求不能被缓存。
  • http响应头中不包含Last-Modified/ETag,也不包含Cache-Control/Expiress的请求无法被缓存。
  • http信息头明确设置Cache-Control:no-cache,pragma:no-cache或Cache-Control:max-age=0浏览器不缓存时。

存是个老生长谈的问题,对于前端工程师来讲更是我们的必修课。或许很多人会说我的项目并没有问题,根本不需要聊什么缓存。如果真的是这样,只能证明你前端道路才刚刚开始。

背景

小郭今天分享缓存的原因在于:公司的一个核心APP中嵌入了SPA,而且应用核心都分布在SPA中,功能复杂且重。问题出现了:应用核心页面打开一直处于加载状态,排除掉弱网环境的原因,重点就在于没有缓存,每次进入页面都需要重载DOM和数据,拖慢页面打开速度。

那应该处理缓存问题呢?接下来小郭从三个方向来讲解。

浏览器缓存策略

在了解浏览器缓存前,我们需要先了解一下相关的概念:cache-control,expires,last-Modified,ETag。

浏览器通过请求头实现缓存,关键的请求头有cache-control,expires,last-Modified,ETag等。我们从时间和空间两个角度来看浏览器缓存。

时间

浏览器发送第一次请求:不缓存,服务端根据设定的缓存策略返回相应的header,如:cache-control,expires,last-Modified,ETag。

浏览器发送第二次请求:

  • 强缓存策略:不需要和服务端通信就决定是否使用缓存,cache-control优先级大于expires① 有cache-control且不过期,返回本地磁盘缓存,状态值200;② 有expires且不过期,返回本地磁盘缓存,状态值200。
  • 协商缓存策略:需要和服务端通信决定是否用缓存,Etag优先级大于last-Modified。① 有Etag,请求头添加If-None-Match,值就是上次返回的Etag值,然后发送给服务端。服务端对比If-None-Match与现有的Etag值是否一样;一样的话只返回header,状态码304,浏览器从本地磁盘获取缓存信息;不一样走正常流程,返回header+body,状态码200;② 有last-Modified,添加请求头If-Modified-Since,值是上次返回的last-Modified,然后发送给服务端。服务端对比If-Modified-Since与现有的是否一样;一样的话返回只返回header,状态码304,浏览器从本地磁盘获取缓存信息;不一样走正常流程,返回header+body,状态码200
  • 无缓存

空间

  • 浏览器和服务端:服务端需要决定使用哪种缓存策略并在响应头返回;前端不需要设置,是浏览器本身机制。
  • html和静态资源:通常html不设置缓存,因为其它资源的入口都是html文件;静态资源(js,css,图片等)会设置缓存

部署时缓存的问题

如果缓存就按理论上设置,那就太简单了。在实际应用有个严重的问题,我们不仅要缓存代码,还需要更新代码。如果静态资源名字不变,怎么让浏览器即能缓存又能在有新代码时更新。最简单的解决方式就是静态资源路径添加一个版本值,版本不变就走缓存策略,版本变了就加载新资源。如下:

<script src="xx/xx.js?v=24334452"></script>

然而这种处理方式在部署时有问题。

解决方法:静态资源和页面是分开部署

  • 先部署页面再部署静态资源,会出现用户访问到旧的资源
  • 先部署静态资源再部署页面,会出现没有缓存用户加载到新资源而报错

这些问题的本质是以上的部署方式是“覆盖式发布”,解决方式是“非覆盖式发布”。即用静态资源的文件摘要信息给文件命名,这样每次更新资源不会覆盖原来的资源,先将资源发布上去。这时候存在两种资源,用户用旧页面访问旧资源,然后再更新页面,用户变成新页面访问新资源,就能做到无缝切换。简单来说就是给静态文件名加hash值

那如何实现呢?

现在前端代码都用webpack之类的构建工具打包,那么结合webpack该怎么做,怎么才能做到持久化缓存?

webpack持久化缓存

一、webpack给文件名添加hash值是很简单的,但hash/chunkhash/contenthash要用哪个呢?

官方定义

hash: unique hash generated for every build

chunkhash: hashes based on each chunks' content

contenthash: hashes generated for extracted content

根据分析,contenthash才是我们需要的,内容有更新,hash值才会更新。

二、webpack会打包业务代码、第三方库及运行时代码,为保证缓存互不干扰,应该将它们提取出来。

第三方库提取方式是设置optimization的splitChunks的cacheGroups。splitChunks能提取模块,cacheGroups能缓存模块,并且cacheGroups的配置会覆盖splitChunks相同配置,既能提取又能缓存,故只需设置cacheGroups。

运行时代码的提取方式为配置runtimeChunk,默认为false,表示运行时代码嵌入到不同的chunk文件中;现在将运行时代码提取出来,并命名为manifest。

module.exports = {
  entry: {
    index: "./src/index.js",
    bar: "./src/bar.js"
  },
  output: {
    filename: "[name].[contenthash].js"
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test:/[\\/]node_modules[\\/]/,
          name: "vendors",
          chunks: "all"
        }
      }
    },
    runtimeChunk: {
      name: "manifest"
    }
  }
};

三、 moduleName 和 chunkName 对文件的影响

module:就是js模块

chunk:webpack编译过程中由多个module组成的文件

bundle:bundle是chunk文件的最终状态,是webpack编译后的结果

一个文件被分离为3个文件,文件间怎么相互依赖的,会影响彼此打包,解决方法是将moduleId和chunkId改成按照文件路径生成。

optimization: {
  moduleIds: 'hashed',
  namedModules: true,
  namedChunks: true
}

这样子moduleId在编译后的文件是文件目录的hash值,更加安全。这也是namedChunks在production默认为false的原因,不想依赖的文件路径在编译后的文件直接展示,但是为了持久性缓存,这里也只能打开。

四、CSS文件缓存

当css代码提取成单独文件,当我们改变css时,怎么保证不影响引用它的js文件呢?配置如下:

plugins: [
  new MiniCssExtractPlugin({
    filename: "[contenthash].css"
  })
]

webpack持久化缓存目标是当且仅当该文件内容变动才改变该文件名字的hash值

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = { 
  output: { 
    filename: [name].[contenthash].js, // 让hash值只在内容变动时更新 
    chunkFilename: [name].[contenthash].js // 动态引入的模块命名,同上 
  }, 
  module: { 
    rules: [ { 
      test: /\.css$/, 
      use: [ 
        "loader: MiniCssExtractPlugin.loader", // 提取出来css "css-loader" 
      ] 
    } ] 
  }, 
  optimization: { 
    moduleIds: "hashed", // 混淆文件路径名 
    runtimeChunk: { name: 'manifest' }, // 提取runtime代码命名为manifest 
    namedModules: true, // 让模块id根据路径设置,避免每增加新模块,所有id都改变,造成缓存失效的情况 
    namedChunks: true, // 避免增加entrypoint,其他文件都缓存失效 
    cacheGroups: { 
      vendor: { // 提取第三方库文件 
        test: /[\\/]node_modules[\\/]/, 
        name: 'vendors', chunks: 'all', 
      }, 
    },
  } 
  plugins: [ 
    new webpack.HashedModuleIdsPlugin(), // 与namedModules: true作用一样 
    new MiniCssExtractPlugin({ 
      filename: "[contenthash].css", // css文件也是按contenthash命名 
      chunkFilename: "[contenthash].css", // 动态引入的css命名,同上 
    }) 
  ], 
}

总结

浏览器有其缓存机制,想要既能缓存又能在部署时没有问题,需要给静态文件名添加hash值。在webpack中,有些配置能让我们实现持久化缓存。感兴趣的同学可以自行去测试哦!

有任何问题可以在下方留言,想了解更多前端知识欢迎关注公众号“一郭鲜”,文章也将同步于公众号,前端学习不迷路


一、是什么
函数缓存,就是将函数运算过的结果进行缓存

本质上就是用空间(缓存存储)换时间(计算过程)

常用于缓存数据计算结果和缓存对象

二、如何实现
实现函数缓存主要依靠闭包、柯里化、高阶函数,这里再简单复习下

1-闭包  
	- 闭包可以理解成,函数 + 函数体内可访问的变量总和
	- add函数本身,以及其内部可访问的变量,即 a = 1,这两个组合在⼀起就形成了闭包
  
    (function() {
        var a = 1;
        function add() {
            const b = 2
            let sum = b + a
            console.log(sum); // 3
        }
        add()
      })()
2-柯里化    
	- 将一个二元函数拆分成两个一元函数
	// 非函数柯里化
    var add = function (x,y) {
        return x+y;
    }
    add(3,4) //7

    // 函数柯里化
    var add2 = function (x) {
        //**返回函数**
        return function (y) {
            return x+y;
        }
    }
    add2(3)(4) //7

3-高阶函数
  - 通过接收其他函数作为参数或返回其他函数的函数
    function foo(){
      var a = 2;

      function bar() {
        console.log(a);
      }
      return bar;
    }
    var baz = foo();
    baz();//2
	- 函数 foo 如何返回另一个函数 bar,baz 现在持有对 foo 中定义的bar 函数的引用。由于闭包特性,a的值能够得到
  
三、应用场景
    虽然使用缓存效率是非常高的,但并不是所有场景都适用,因此千万不要极端的将所有函数都添加缓存

    以下几种情况下,适合使用缓存:

    对于昂贵的函数调用,执行复杂计算的函数
    对于具有有限且高度重复输入范围的函数
    对于具有重复输入值的递归函数
    对于纯函数,即每次使用特定输入调用时返回相同输出的函数

、内存泄漏