前端性能优化之缓存技术

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

从HTTP协议开始说起

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

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

具体的流程可以看下面这张图(该图来自HTTP权威指南):

http请求缓存流程图

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

强缓存

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

协商缓存

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

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

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

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

需要注意的是当响应头中同时存在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脚本情况:
[图片上传失败...(image-8a2381-1514774903030)]

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年的开篇之作,希望各位读者在新的一年里都能工作顺利,生活快乐!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容