前端性能優化之緩存利用

緩存種類

大體想了一下,關於緩存,無非有如下幾種:

1、CDN緩存

2、DNS緩存

3、客戶端緩存(無需請求的memory cache,disk cache,需要發請求驗證的Etag、Last-Modified304)

4、Service Worker與緩存及離線緩存

5、PageCache與ajax緩存

CDN緩存

看到一個形象的比喻,來比喻CDN。

10年前,還沒有火車票代售點一說,12306.cn更是無從說起。那時候火車票還只能在火車站的售票大廳購買,而我所在的小縣城並不通火車,火車票都要去市裏的火車站購買,而從我家到縣城再到市裏,來回就是4個小時車程,簡直就是浪費生命。後來就好了,小縣城裏出現了火車票代售點,甚至鄉鎮上也有了代售點,可以直接在代售點購買火車票,方便了不少,全市人民再也不用在一個點苦逼的排隊買票了。

CDN就可以理解爲分佈在每個縣城或者鄉鎮的火車票代售點,用戶在瀏覽網站的時候,CDN會選擇一個離用戶最近的CDN邊緣節點來響應用戶的請求,這樣海南移動用戶的請求就不會千里迢迢跑到北京電信機房的服務器(假設源站部署在北京電信機房)上了。

CDN的優勢很明顯:(1)CDN節點解決了跨運營商和跨地域訪問的問題,訪問延時大大降低;(2)大部分請求在CDN邊緣節點完成,CDN起到了分流作用,減輕了源站的負載。

關於CDN緩存 ,在瀏覽器本地緩存失效後,瀏覽器會向CDN邊緣節點發起請求。類似瀏覽器緩存,CDN邊緣節點也存在着一套緩存機制。CDN邊緣節點緩存策略因服務商不同而不同,但一般都會遵循http標準協議,通過http響應頭中的

Cache-control: max-age

的字段來設置CDN邊緣節點數據緩存時間。

當客戶端向CDN節點請求數據時,CDN節點會判斷緩存數據是否過期,若緩存數據並沒有過期,則直接將緩存數據返回給客戶端;否則,CDN節點就會向源站發出回源請求,從源站拉取最新數據,更新本地緩存,並將最新數據返回給客戶端。 CDN服務商一般會提供基於文件後綴、目錄多個維度來指定CDN緩存時間,爲用戶提供更精細化的緩存管理。

CDN緩存刷新

CDN邊緣節點對開發者是透明的,相比於瀏覽器Ctrl+F5的強制刷新來使瀏覽器本地緩存失效,開發者可以通過CDN服務商提供的“刷新緩存”接口來達到清理CDN邊緣節點緩存的目的。這樣開發者在更新數據後,可以使用“刷新緩存”功能來強制CDN節點上的數據緩存過期,保證客戶端在訪問時,拉取到最新的數據。

DNS緩存

DNS(Domain Name System): 負責將域名URL轉化爲服務器主機IP。

DNS查找流程:首先查看瀏覽器緩存是否存在,不存在則訪問本機DNS緩存,再不存在則訪問本地DNS服務器。所以DNS也是開銷,通常瀏覽器查找一個給定URL的IP地址要花費20-120ms,在DNS查找完成前,瀏覽器不能從host那裏下載任何東西。

TTL(Time To Live):表示查找返回的DNS記錄包含的一個存活時間,過期則這個DNS記錄將被拋棄。瀏覽器DNS緩存也有自己的過期時間,這個時間是獨立於本機DNS緩存的,相對也比較短,例如chrome只有1分鐘左右。

DNS性能優化最佳實踐

當客戶端的DNS緩存爲空時,DNS查找的數量與Web頁面中唯一主機名的數量相等。所以減少唯一主機名的數量就可以減少DNS查找的數量。

但是問題來了,有時候需要多設置主機數量,來增加DNS的負載均衡,因此減少DNS查找和增加主機數量形成了矛盾關係,經過實戰DNS設置2-4個主機名是最佳的。更多負載均衡可以用其他方式實現,例如用nginx做負載均衡!

瀏覽器緩存策略之客戶端緩存

1、Cache-control: max-age

假設你的站點有引用一個腳本文件,你非常確認這個腳本文件內容五十年不變。那麼自然希望瀏覽器把這個腳本緩存起來,不用每一次都請求服務器,然後服務器再返回相同的內容。這樣能夠節省帶寬開銷並且提升性能。

此時你只需要設置文件返回的HTTP頭中的Cache-Control設置爲:

Cache-Control: max-age=31536000

雖然是五十年不變,但是標準中規定max-age值最大不能超過一年,又因爲是以秒爲單位,所以值爲31536000

例如這個五十年不變的腳本地址是 www.haorooms.com/never-expire.js 那麼接下來每次用戶請求這個地址時,瀏覽器都不會再向服務器發出請求,而是直接從本地的瀏覽器緩存中取。直到一年以後或者用戶手動的清除了緩存。

但是,如果這一年中的某一天你發現腳本內容必須要更改了怎麼辦?很簡單,改變請求的文件名就好了,例如never-expire-v2.js。

Cache-control: max-age 可以控制緩存時間。如下圖:

enter image description here

Max-age 使用秒來計量,如:

Cache-Control:max-age=645672

指定頁面645672秒(7.47天)後過期。

2、Expires

enter image description here

設置了Expires也可以避免瀏覽器和服務器發請求,直到時間過期。

以上2個緩存,遵循三級緩存原理

3、Last-Modified 304協商緩存

服務器爲了通知瀏覽器當前文件的版本,會發送一個上次修改時間的標籤,例如:

Last-Modified:Tue, 06 Jan 2018 08:26:32 GMT

如下圖:

enter image description here

假如是304協商緩存,驗證步驟如下:

  1. 瀏覽器:Hey,我需要jquery.min.js這個文件,如果是在 Last-Modified:Tue, 06 Jan 2018 08:26:32 GMT 之後修改過的,請發給我。

  2. 服務器:(檢查文件的修改時間)

  3. 服務器:Hey,這個文件在那個時間之後沒有被修改過,你已經有最新的版本了。

  4. 瀏覽器:太好了,那我就顯示給用戶了。

4、ETag

上面截圖中也圈出來了,其實Etag和304類似,但是級別比 Last-Modified 高一些。

請求過程如下:

1. 瀏覽器:Hey,我需要haoroomsmain.css這個文件,有沒有不匹配"61213-1762a-50bf790757204"這個串的

2. 服務器:(檢查ETag…)

3. 服務器:Hey,我這裏的版本也是"61213-1762a-50bf790757204",你已經是最新的版本了

4. 瀏覽器:好,那就可以使用本地緩存了

Service Worker與緩存及離線緩存

隨着Service Worker(以下簡稱SW)的普及和規範,我們可以使用SW提供的緩存接口替代HTTP緩存。當然SW的功能是強大的,除了緩存功能,還能夠使用它來實現離線、數據同步、後臺編譯等等。

一個標配版的sw緩存工代代碼應該有以下的片段:

const version = '2';

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(`static-${version}`)
      .then(cache => cache.addAll([
        '/styles.css',
        '/script.js'
      ]))
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request))
  );
});

首先你要明白的前提是,網絡請求首先到達的是SW腳本中,如果未命中再轉發給HTTP緩存。

這段代碼的意思是,在SW的install階段我們將script.js和styles.css放入緩存中;而在請求發起的fetch階段,通過資源的URL去緩存內查找匹配,成功後立刻返回,否則走正常的網絡請求流程。

但你有沒有考慮過,在install階段的資源內容是哪裏來的?仍然是從HTTP緩存中。這樣SW緩存機制又有可能隨着HTTP緩存陷入了之前所說的版本不一致的困境中。

既然我們藉助SW重寫了緩存機制,所以也不想再受牽制於舊的HTTP緩存。解決辦法是讓SW中的請求必須向服務端驗證:

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(`static-${version}`)
      .then(cache => cache.addAll([
        new Request('/styles.css', { cache: 'no-cache' }),
        new Request('/script.js', { cache: 'no-cache' })
      ]))
  );
});

目前並非所有的瀏覽器都支持cache選項的配置。但這個不是太大問題,我們可以通過添加隨機數來保證每次請求的URL都不相同,間接的使得緩存失效:

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(`static-${version}`)
      .then(cache => Promise.all(
        [
          '/styles.css',
          '/script.js'
        ].map(url => {
          // cache-bust using a random query string
          return fetch(`${url}?${Math.random()}`).then(response => {
            // fail on 404, 500 etc
            if (!response.ok) throw Error('Not ok');
            return cache.put(url, response);
          })
        })
      ))
  );
});

離線緩存

關於離線緩存,可以看我之前的文章:http://www.haorooms.com/post/html5_appcache

PageCache與Ajax可緩存

PageCache其實是facebook提出的,解決ajax緩存的一種方案!簡單的說,就是將訪問過的頁面緩存在客戶端。但我們知道,作爲Facebook這樣交互性很強的網站,需要保障用戶能儘早的獲得更新後的信息,而不是給用戶展示一個毫無意義的過期頁面。

Facebook設計了一個框架來識別一個頁面是否來自於緩存(猜測:頁面首次加載完畢後將所有Ajax的Callback和Result緩存在本地。Facebook頁面是基於Ajax獲取頁面內容,參見BigPipe),若來自於緩存,通過Ajax來更新所需更新的模塊(猜測:通過JS預先定義本頁面所需更新的div Id及對應的callback handler,並在頁面下載時同時下載下來)。

其提到了三種更新類型:增量更新,用戶複寫(例如用戶在頁面上回復了一則評論)及跨頁更新(例如在消息詳細頁面將一則消息標識爲已讀,需將首頁的未讀消息數進行更新。)。核心思路還是依據Ajax進行更新。具體思路爲:

1、增量更新:只要頁面來自於緩存,即更新所有預定義的需增量更新的模塊。

2、用戶複寫:通過HistoryManager記錄用戶操作並在cache頁面讀取後重放所有被標記爲“replayable”的操作。

3、跨頁更新:通過服務端Database API發送信號至客戶端將過期緩存標識爲invalid(不清楚如何實現。也許是DB端提供一個開放的webservice,客戶端通過Ajax持續訪問此API來獲得此信息)。獲得了緩存過期信號後,通過Ajax更新需要更新的信息。

Facebook順帶提到了一個更新Ajax內容避免頁面變化/閃爍的小技巧,就是先將需更新的地方設置爲blank,而非直接更新其內容。

原文鏈接:https://www.haorooms.com/post/cache_huancunliyong


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章