前端性能优化之缓存技术

缓存一直以来都是用来提升性能的一项必不可少的技术 , 利用这项技术能够很好地提升web的性能。 缓存能够颇有效地下降网络的时延,同时也会减小大量请求对于服务器的压力。 接下来这篇文章将会详细地介绍在web领域中缓存的一些知识点和应用。javascript

从HTTP协议开始提及

因为整个网络服务都是基于http协议 的,所以先来介绍一下HTTP协议当中定义的缓存机制。HTTP协议主要是经过请求头当中的一些字段来和服务器进行通讯,从而采用不一样的缓存策略。css

通常来讲,对于一个完整的HTTP GET请求缓存过程会包含七个主要的步骤:①从接收网络请求开始,②客户端会读取请求报文而且对报文进行解析, 进而提取URL和各类首部,③而后将会查询是否在本地有副本,若是本地没有副本就会从服务器上获取一份副本而且保存在本地。④接着会进行查看副本是否足够新鲜(新鲜度检测), 若是缓存已经失效就会询问服务器是否有任何更新,⑤服务器就会用新的首部和已缓存的主体来构建一条响应报文,⑥最后发送给客户端。⑦根据服务器的不一样,会可选地选择建立日志记录该过程。html

具体的流程能够看下面这张图(该图来自HTTP权威指南):前端

http请求缓存流程图

根据缓存处理方式的不一样,接着又会分为两类:强缓存和协商缓存。java

强缓存

强缓存主要是采用响应头中的Cache-Control和Expires两个字段进行控制的。其中Expires是HTTP 1.0中定义的,它指定了一个绝对的过时时期。而Cache-Control是HTTP 1.1时出现的缓存控制字段。Cache-Control:max-age定义了一个最大使用期,就是从第一次生成文档到缓存再也不生效的合法生存日期。因为Expires是HTTP1.0时代的产物,所以设计之初就存在着一些缺陷,若是本地时间和服务器时间相差太大,就会致使缓存错乱。这两个字段同时使用的时候Cache-Control的优先级给更高一点。 这两个字段的效果是相似的,客户端都会经过对比本地时间和服务器生存时间来检测缓存是否可用。若是缓存没有超出它的生存时间内,客户端就会直接采用本地的缓存。若是生存日期已通过了,这个缓存也就宣告失效。接着客户端将再次与服务器进行通讯来验证这个缓存是否须要更新。jquery

协商缓存

强缓存机制若是检测到缓存失效,就须要进行服务器再验证。这种缓存机制也称做协商缓存。浏览器在第一次获取请求的时候,就会在响应头中携带上资源的上次服务器修改日期(Last-Modified)或者资源的标签(Etag)。后续的请求服务器会根据请求头上的If-Modified-Since(对应Last-Modified)和(If-None-Match)字段来判断资源是否失效,一旦资源过时,则服务器会从新发送新的资源到客户端上,从而保证资源的有效性。webpack

其中Last-Modified字段对应的是资源最后修改时间,例如:`Last-Modified:git

Sat, 30 Dec 2017 20:18:56 GMT` ,当客户端再次请求该资源的时候,会在其请求头上附带上If-Modified-Since字段,值就是以前返回的Last-Modified值。若是资源未过时,命中缓存,服务器就直接返回304状态码,客户端直接使用本地的资源。不然,服务器从新发送响应资源。github

另一种协商缓存的校验方式的经过校验码而不是时间,这样就保证了在文件内容不变的状况下不会重复占用网络资源。响应头中Etag字段是服务器给资源打上的一个标记,利用这个标记就能够实现缓存的更新。后续发起的请求,会在请求头上附带上If-None-Match字段,其值就是这个标记的值。web

须要注意的是当响应头中同时存在Etag和Last-Modified的时候,会先对Etag进行比对,随后才是Last-Modified。

浏览器缓存

上面介绍了网络协议层面的缓存方案,接下来从前端的角度来看一下浏览器中几种经常使用的缓存技术。

localstorage

原本HTTP协议的缓存方案很美好了,不过当用户主动触发页面刷新内容,如:F5等,就会使浏览器的强缓存失效,进而转变成协商缓存。而利用LocalStorage能够无视用户主动刷新行为,而且能够存储较大致积的资源(2M以上)。

localStorage的使用也较为简单:

const key = 'scq000';
const value = 'hello world';

// 存
localStorage.setItem(key, value);

// 取
localStorage.getItem(key);
复制代码

虽然说localStorage通常是用来存储应用数据的,可是也能够利用其存储js和css等静态资源。

<script id="testJs" src="example.js"></script>
复制代码
// 以js为例
var lsKey = 'loadJSv1.0'; // 做为localStorage存取的key;

// 获取要缓存或者执行的源码内容
function getScriptContent(url, callback) {
    var httpRequest = new XMLHttpRequest();
	httpRequest.onreadystatechange = function() {
        if (httpRequest.readyState === 4) {
            if (httpRequest.status === 200) {
            	// 获取代码内容
                var codeStr = httpRequest.responseText;
          	    callback && callback(codeStr);
            }
        }
    };
	httpRequest.open('GET', url);
	httpRequest.send();
}

// 第一次运行的时候缓存
function cacheJs(url) {
  	// 获取代码内容 
  	getScriptContent(url, function(codeStr) {
        console.log(codeStr);
        // 执行代码并缓存
        var script = document.createElement('script');
		script.innerHTML = codeStr;
  		localStorage.setItem(lsKey, codeStr);
    });
}

// 加载源码
function loadJs(url) {
    // 读取缓存
 	var cacheStr = localStorage.getItem(lsKey);
  	if(cacheStr) {
      	// 插入浏览器中,或者也能够直接使用eval执行
        var script = document.createElement('script');
		script.innerHTML = cacheStr;
        console.log("使用缓存成功");
    } else {
        // 没有缓存,就会从服务器获取源码并缓存到本地
        cacheJs(url);
    }
}

// 第一次执行的时候,会直接执行并缓存到localhost中去,第二次进入的时候,会直接使用缓存
loadJs('http://code.jquery.com/jquery-3.2.1.min.js')

复制代码

上面只是一个简单的demo,若是真的要使用这种方案,还须要考虑到更新处理问题。

做为一种性能优化的方案,这种方法也曾被大量应用于移动端的网页中。不过缺点也很明显,因为localStorage是保存在本地中的,因此很容易致使xss注入攻击。若是要使用这种方案,必定要作好对应的安全措施。在这里推荐一篇文章:使用 SRI 加强 localStorage 代码安全

App Cache方案

HTML5曾经提供了一个应用程序缓存机制, 使得基于web的应用程序能够离线运行。这就是App Cache(采用mainfest文件进行缓存), 因为方案目前正在从web标准中删除,因此在这里只作简单的介绍。

  1. 新建一个html文件的时候,添加mainfest属性,而且指定缓存清单文件,这个文件是在应用处于离线状态时使用的。
<!DOCTYPE html>
<html manifest="index.appcache">
<head>
	<title></title>
</head>
<body>

</body>
</html>
复制代码
  1. 新建缓存清单文件index.appcache
CACHE MANIFEST
# v1 - 2017-11-11 
# 缓存版本号

# 指定须要被缓存的文件
CACHE:
index.html
script.js

# 指定须要和服务器链接的白名单,将不进行缓存
NETWORK:
style.css

# 回退页面,当资源没法访问,浏览器将采用该页面
FALLBACK:
index_bak.html

复制代码

这个方案一个比较很差的地方,是须要和服务器进行配合,mainfest文件的版本更新也是一个问题,同时资源还不支持部分更新。若是你想了解更多,能够访问Using the application cache

Service Worker

做为AppCache的替代方案,Service Worker 是一个相对来讲比较新的技术,其目的也主要是为了提升web app的用户体验,能够实现离线应用消息推送等等一系列的功能, 从而进一步缩小与原生应用的差距。 Service Worker能够看作是一个独立于浏览器的Javascript代理脚本,经过JS的控制,可以使应用先获取本地缓存资源(Offline First),从而在离线状态下也能提供基本的功能。 出于安全性的考虑,Service Worker 只能在https协议下使用,不过为了便于开发,如今的浏览器默认支持localhost使用Service Worker。

Service Worker整个的使用过程包括了注册,安装,激活,睡眠销毁等等一系列的状态。

注册

首先须要在页面中注册一个Service Worker。须要写在入口文件中:

if(‘serviceWorker' in navigator) {
  navigator.serviceWorker.register('./testSW.js', {scope: '/src'}).then(reg => {
	console.log('service worker is working', reg);    
  }).catch(e => console.log('register service worker failed'));
}
复制代码

因为兼容性的问题,须要在代码开始作浏览器特性检测处理。注册时候,scope参数是可选的,用来限制SW的工做范围的。

安装和激活

// 用来标记缓存
const CACHE_FILE = 'my-sw-demo-v1';
let filesToCache = [
  '/',
  '/index.html',
  '/scripts/main.js',
  '/styles/main.css'
];

// 安装
self.addEventListener('install', event => {
  event.waitUntil(
  	caches.open(CACHE_FILE)
  		.then(cache => cache.addAll(filesToCache));
  );
});

// 添加fetch事件监听
self.addEventListener('fetch', event => {
  event.responseWith(
  	caches.match(event.request)
  		.then(response => response)
  		.catch(() => fetch(event.request));
  );
});
复制代码

当用户首次访问页面的时候,会触发SW的安装事件,addAll方法接收须要被缓存文件的url列表,并会自动获取这些文件存入缓存中。 接下来注册的fetch事件监听器会在每次SW被控制的资源请求时触发,拦截请求并在缓存中匹配对应资源。若是缓存命中,则直接返回资源,不然去发起fetch请求。 固然,若是你想更进一步,能够在缓存没有命中的时候,获取资源而后将获取到资源加入缓存中。另外,在网络不可用的时候,提供一种回退方案。上面的代码能够改写成这样:

// 添加fetch事件监听
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).catch(() => {
      return fetch(event.request).then(response => {
        return caches.open('v1').then(cache => {
          cache.put(event.request, response.clone());
          return response;
        });
      });
    }).catch(() => {
      // 回退资源
      return caches.match('/fallback.html');
    })
  );
});

复制代码

更新资源

若是应用中SW已经安装,可是刷新的时候检测到有新版保持可用,就会自动安装。可是须要注意的是,只有当再也不有任何已加载页面在使用旧版SW的时候,新版本的SW才会被激活。

我们把上面的版本号更改一下:

const CACHE_FILE = 'my-sw-demo-v2';
复制代码

此时刷新页面,当install事件发生的时候,前一个版本(my-sw-demo-v1)若是还在被其它页面使用,则这个新版本不会被激活,当全部页面都再也不使用v1的时候,v2就会激活并开始响应请求。

删除旧缓存

做为缓存的完整生命周期来讲,提供删除功能必不可少。咱们有时候须要手动删除旧版本的缓存,以便释放有限的浏览器缓存空间。此时,能够利用activate事件和waitUntil这样一个方法来清理缓存。

// 清理缓存操做
self.addEventListener('activate', event => {
  // 设置白名单,不须要删除的缓存key
  const cacheWhiteList = ['v2'];
  
  event.waitUntil(
  	cache.keys().then(keyList => {
      return Promise.all(keyList.map(key => {
        if (!cacheWhiteList.includes(key)) {
          // 若是不在白名单里面,就删除该缓存
          return cache.delete(key);
        }
      }));
  	});
  )
});
复制代码

调试工具

调试的时候,能够在谷歌浏览器中输入chrome://serviceworker-internals/查看各个页面SW脚本的工做状况。也能够在开发者工具中查看当前页的SW脚本状况:

SW目前仍是一个草案,在PC端上各个浏览器的支持度并非很高,可是在手机端已大部分可以实现支持了。做为PWA的一种核心技术,谷歌对SW提供不少颇有用的工具,如:Sw-precache, Sw-toolbox,感兴趣的能够去研究一番。 下面收集了一些比较有用的工具和参考文章,若是须要深刻学习,能够一阅: serviceworker-webpack-plugin

https://www.npmjs.com/package/workbox-webpack-plugin

http://air.ghost.io/using-workbox-webpack-to-precache-with-service-worker/

https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API

https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers

https://ivweb.io/topic/5876d4ee441a881744b0d6d6

https://x5.tencent.com/tbs/guide/serviceworker.html

https://foio.github.io/

https://huangxuan.me/2017/07/12/upgrading-eleme-to-pwa/

最后,做为2018年的开篇之做,但愿各位读者在新的一年里都能工做顺利,生活快乐!

相关文章
相关标签/搜索