前端效能優化之快取技術

scq000發表於2019-03-03

快取一直以來都是用來提高效能的一項必不可少的技術 , 利用這項技術可以很好地提高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指令碼情況:

前端效能優化之快取技術

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年的開篇之作,希望各位讀者在新的一年裡都能工作順利,生活快樂!

相關文章